added ruff test
This commit is contained in:
@@ -9,21 +9,6 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QAction, QActionGroup, QIcon, QImage
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QSplitter,
|
||||
QStatusBar,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
DEPTH,
|
||||
@@ -39,24 +24,33 @@ from orisys_app_shared.force import select_display_forces
|
||||
from orisys_app_shared.force.calibration_recorder import CalibrationRecorder
|
||||
from orisys_app_shared.force.filters import FilterSettings
|
||||
from orisys_app_shared.force.session import SessionStore
|
||||
from orisys_app_shared.paths import recordings_dir
|
||||
from orisys_app_shared.i18n import set_locale, t
|
||||
from orisys_app_shared.logging import report_error
|
||||
from orisys_app_shared.paths import logo_path, recordings_dir, sensor_json_path
|
||||
from orisys_app_shared.prefs import save_preferences
|
||||
from orisys_app_shared.record import RecordConfig
|
||||
from orisys_app_shared.record.sample import to_record_sample
|
||||
from orisys_app_shared.theme import build_qss, clear_theme_cache, color
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QAction, QActionGroup, QIcon
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QSplitter,
|
||||
QStatusBar,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_studio import session_metadata
|
||||
from orisys_studio.calibration_service import CalibrationService
|
||||
from orisys_studio.display_prefs import DisplayPrefsController
|
||||
from orisys_studio.recording import RecordingCoordinator, record_config_from_prefs
|
||||
from orisys_studio.replay import ReplayEngine
|
||||
from orisys_studio.replay_controller import ReplayController
|
||||
from orisys_studio.calibration_service import CalibrationService
|
||||
from orisys_studio.display_prefs import DisplayPrefsController
|
||||
from orisys_studio.view_layout import ViewLayoutController
|
||||
from orisys_studio import session_metadata
|
||||
from orisys_studio.session_controller import SessionController
|
||||
from orisys_studio.scene3d.prefs import settings_to_prefs
|
||||
from orisys_app_shared.i18n import set_locale, t
|
||||
from orisys_app_shared.logging import report_error
|
||||
from orisys_app_shared.paths import logo_path, sensor_json_path
|
||||
from orisys_app_shared.prefs import save_preferences
|
||||
from orisys_app_shared.theme import build_qss, clear_theme_cache, color
|
||||
|
||||
from orisys_studio.session_controller import SessionController
|
||||
from orisys_studio.ui import record_status as record_states
|
||||
from orisys_studio.ui.about_dialog import AboutDialog
|
||||
from orisys_studio.ui.arrow_panel import ArrowPanel, bgr_to_qimage
|
||||
@@ -74,6 +68,7 @@ from orisys_studio.ui.plot_settings_dialog import (
|
||||
)
|
||||
from orisys_studio.ui.sensor_picker import SensorPickerWidget
|
||||
from orisys_studio.ui.toolbar import ToolbarWidget
|
||||
from orisys_studio.view_layout import ViewLayoutController
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -2476,7 +2471,7 @@ class StudioMainWindow(QMainWindow):
|
||||
return ""
|
||||
return t("panels.depth_scale", low=f"{span[0]:.3f}", high=f"{span[1]:.3f}")
|
||||
|
||||
def _latched_domain_mask(self, frame: "ImageFrame"):
|
||||
def _latched_domain_mask(self, frame: ImageFrame):
|
||||
"""The 2D domain boundary to draw, latched for the session.
|
||||
|
||||
Exactly the treatment ``_depth_range`` gives the colour scale, and for the same
|
||||
|
||||
@@ -15,7 +15,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.force.calibration_io import (
|
||||
CHANNEL_KEYS,
|
||||
@@ -37,14 +37,14 @@ logger = logging.getLogger(__name__)
|
||||
class CalibrationService:
|
||||
"""Owns the calibration session's files and its prepared model."""
|
||||
|
||||
def __init__(self, *, store: Optional[CalSessionStore] = None) -> None:
|
||||
def __init__(self, *, store: CalSessionStore | None = None) -> None:
|
||||
self.store = store or CalSessionStore()
|
||||
#: The one prepared model the panel displays and exports.
|
||||
self.prepared: Any = None
|
||||
#: Device identity of a reopened record, while it is open.
|
||||
self.record_identity: Optional[dict] = None
|
||||
self.record_identity: dict | None = None
|
||||
#: The record currently open, if any.
|
||||
self.open_record: Optional[CalibrationSessionRecord] = None
|
||||
self.open_record: CalibrationSessionRecord | None = None
|
||||
|
||||
# --------------------------------------------------------------- identity
|
||||
def export_meta(
|
||||
|
||||
@@ -19,17 +19,17 @@ repaint when the 2D view mode changes.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional, Protocol
|
||||
from typing import Any, Protocol
|
||||
|
||||
from PySide6.QtCore import QObject
|
||||
|
||||
from orisys_studio.ui.plot_settings_dialog import (
|
||||
DEFAULT_PLOT_DEADBAND_N,
|
||||
DEFAULT_PLOT_DEADBAND_RAW,
|
||||
DEFAULT_PLOT_FN_MIN,
|
||||
DEFAULT_PLOT_FN_MAX,
|
||||
DEFAULT_PLOT_FN_RAW_MIN,
|
||||
DEFAULT_PLOT_FN_MIN,
|
||||
DEFAULT_PLOT_FN_RAW_MAX,
|
||||
DEFAULT_PLOT_FN_RAW_MIN,
|
||||
DEFAULT_PLOT_SHEAR_MAX,
|
||||
DEFAULT_PLOT_SHEAR_RAW_MAX,
|
||||
DEFAULT_PLOT_WINDOW_SEC,
|
||||
@@ -66,7 +66,7 @@ class DisplayHost(Protocol):
|
||||
prefs: dict
|
||||
|
||||
@property
|
||||
def _latest_fps(self) -> Optional[float]: ...
|
||||
def _latest_fps(self) -> float | None: ...
|
||||
|
||||
def _persist_prefs(self) -> None: ...
|
||||
|
||||
@@ -94,7 +94,7 @@ class DisplayPrefsController(QObject):
|
||||
arrow_rotate_types: frozenset,
|
||||
mesh_panel: Any = None,
|
||||
rotate_arrows_action: Any = None,
|
||||
parent: Optional[QObject] = None,
|
||||
parent: QObject | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self._host = host
|
||||
|
||||
@@ -11,17 +11,18 @@ import sys
|
||||
# wrong one. The application is PySide6-only, so say so before anything imports pyqtgraph.
|
||||
os.environ.setdefault("PYQTGRAPH_QT_LIB", "PySide6")
|
||||
|
||||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||||
|
||||
from orisys_app_shared import API_VERSION, __version__ as shared_version
|
||||
from orisys_app_shared import API_VERSION
|
||||
from orisys_app_shared import __version__ as shared_version
|
||||
from orisys_app_shared.compat import check_shared_compat
|
||||
from orisys_app_shared.i18n import set_locale
|
||||
from orisys_app_shared.logging import init_logging, install_exception_hooks
|
||||
from orisys_app_shared.paths import prefer_local_sensor_bundles
|
||||
from orisys_app_shared.prefs import load_preferences
|
||||
from orisys_app_shared.theme import build_qss
|
||||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||||
|
||||
from orisys_studio import REQUIRES_SHARED_API, __version__ as app_version
|
||||
from orisys_studio import REQUIRES_SHARED_API
|
||||
from orisys_studio import __version__ as app_version
|
||||
from orisys_studio.app_window import StudioMainWindow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,8 +16,9 @@ here, where they can be tested without a display.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.record import ExperimentRecorder, RecordConfig, RecordReport
|
||||
from orisys_app_shared.record.calibration_snapshot import store_calibration_snapshot
|
||||
@@ -34,7 +35,7 @@ class RecordingCoordinator:
|
||||
*,
|
||||
session_store,
|
||||
cal_recorder,
|
||||
recorder: Optional[ExperimentRecorder] = None,
|
||||
recorder: ExperimentRecorder | None = None,
|
||||
) -> None:
|
||||
self.session_store = session_store
|
||||
self.cal_recorder = cal_recorder
|
||||
@@ -47,7 +48,7 @@ class RecordingCoordinator:
|
||||
self._alarms: list[str] = []
|
||||
#: A stop was requested and the writer has not closed its files yet.
|
||||
self.report_pending = False
|
||||
self.last_report: Optional[RecordReport] = None
|
||||
self.last_report: RecordReport | None = None
|
||||
self._active_calibration_id = ""
|
||||
self._record_fps = 30.0
|
||||
self.recorder.set_alarm_handler(self._on_alarm)
|
||||
@@ -210,7 +211,7 @@ class RecordingCoordinator:
|
||||
self.report_pending = False
|
||||
return Path(directory)
|
||||
|
||||
def stop(self, *, wait: bool = True) -> Optional[RecordReport]:
|
||||
def stop(self, *, wait: bool = True) -> RecordReport | None:
|
||||
"""Stop and return the report, or ``None`` when nothing was started."""
|
||||
if self.recorder.directory is None and self.recorder.report is None:
|
||||
self.report_pending = False
|
||||
@@ -221,7 +222,7 @@ class RecordingCoordinator:
|
||||
self.publish(report)
|
||||
return report
|
||||
|
||||
def publish(self, report: Optional[RecordReport]) -> None:
|
||||
def publish(self, report: RecordReport | None) -> None:
|
||||
"""Record a report; a completed one becomes ``last_report``."""
|
||||
if report is None:
|
||||
return
|
||||
@@ -232,7 +233,7 @@ class RecordingCoordinator:
|
||||
self.report_pending = False
|
||||
self.last_report = report
|
||||
|
||||
def poll(self) -> Optional[RecordReport]:
|
||||
def poll(self) -> RecordReport | None:
|
||||
"""The terminal report, once the writer has actually closed the files."""
|
||||
if not self.report_pending:
|
||||
return None
|
||||
@@ -256,7 +257,7 @@ def record_config_from_prefs(
|
||||
*,
|
||||
mode: str,
|
||||
fps: float,
|
||||
on_problem: Optional[Callable[[str], None]] = None,
|
||||
on_problem: Callable[[str], None] | None = None,
|
||||
) -> RecordConfig:
|
||||
"""Build the record config the toolbar/prefs describe."""
|
||||
return RecordConfig(
|
||||
|
||||
@@ -21,13 +21,11 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.record import RecordingReader, VideoOnlySource
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,7 +62,7 @@ class ReplayEngine(QObject):
|
||||
self._position = 0.0
|
||||
#: The frame cache: one video image per cursor position.
|
||||
self._video_position: float | None = None
|
||||
self._video_bgr: Optional[np.ndarray] = None
|
||||
self._video_bgr: np.ndarray | None = None
|
||||
self._video_reason = ""
|
||||
self._warnings = ""
|
||||
|
||||
@@ -209,7 +207,7 @@ class ReplayEngine(QObject):
|
||||
return self._unit
|
||||
|
||||
# ----------------------------------------------------------------- frames
|
||||
def video_frame_bgr(self) -> Optional[np.ndarray]:
|
||||
def video_frame_bgr(self) -> np.ndarray | None:
|
||||
"""The recorded frame under the cursor, as BGR.
|
||||
|
||||
The engine hands over pixels, not a prepared widget image: the single 2D
|
||||
|
||||
@@ -25,15 +25,14 @@ import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Protocol
|
||||
from typing import Any, Protocol
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import QObject, QTimer
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
|
||||
from orisys_app_shared.acquisition.image_backend import SOURCE_REPLAY, ImageFrame
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.paths import recordings_dir
|
||||
from PySide6.QtCore import QObject, QTimer
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -111,7 +110,7 @@ class ReplayController(QObject):
|
||||
charts: Any,
|
||||
image_panel: Any,
|
||||
mesh_panel: Any,
|
||||
parent: Optional[QObject] = None,
|
||||
parent: QObject | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self._engine = engine
|
||||
@@ -120,7 +119,7 @@ class ReplayController(QObject):
|
||||
self._charts = charts
|
||||
self._image_panel = image_panel
|
||||
self._mesh_panel = mesh_panel
|
||||
self._actions: Optional[ReplayActions] = None
|
||||
self._actions: ReplayActions | None = None
|
||||
#: True while a recompute pass is running: it is a *run* over a recording, so
|
||||
#: the acquire pipeline owns the panels and the transport stops the pass.
|
||||
self._recompute_running = False
|
||||
@@ -501,7 +500,7 @@ class ReplayController(QObject):
|
||||
self._host._notify(t("toolbar.nothing_to_stop"))
|
||||
|
||||
# ---------------------------------------------------------------- recompute
|
||||
def video_segment(self) -> Optional[Path]:
|
||||
def video_segment(self) -> Path | None:
|
||||
"""The recorded video a recompute pass should read."""
|
||||
reader = self._engine.reader()
|
||||
segments = sorted(reader.segments()) if reader is not None else []
|
||||
|
||||
@@ -33,10 +33,9 @@ Two things the v1 adapters deliberately do **not** do:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.acquisition.models import SdkPacket
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
Diagnostic,
|
||||
@@ -45,7 +44,6 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
FrameIdentity,
|
||||
SurfaceFieldFrame,
|
||||
diagnostic,
|
||||
error,
|
||||
freeze,
|
||||
)
|
||||
from orisys_app_shared.scene3d.sampling import remap_to_domain
|
||||
@@ -84,7 +82,7 @@ def _refusal_rank(item: Diagnostic) -> int:
|
||||
return _REFUSAL_RANK.get(str(item.code), 0)
|
||||
|
||||
|
||||
def flow_magnitude(packet: SdkPacket) -> Optional[np.ndarray]:
|
||||
def flow_magnitude(packet: SdkPacket) -> np.ndarray | None:
|
||||
"""``|flow|`` as float32, or ``None`` when the packet carries no flow."""
|
||||
flow = getattr(packet, "flow", None)
|
||||
if flow is None:
|
||||
@@ -97,11 +95,11 @@ def flow_magnitude(packet: SdkPacket) -> Optional[np.ndarray]:
|
||||
return None
|
||||
|
||||
|
||||
def _shape_of(array: Optional[np.ndarray]) -> str:
|
||||
def _shape_of(array: np.ndarray | None) -> str:
|
||||
return "none" if array is None else "x".join(str(int(v)) for v in array.shape[:2])
|
||||
|
||||
|
||||
def _field_validity(packet: SdkPacket, field: str, frame_id: int) -> Optional[Diagnostic]:
|
||||
def _field_validity(packet: SdkPacket, field: str, frame_id: int) -> Diagnostic | None:
|
||||
"""Whether the packet may claim ``field`` for *this* frame.
|
||||
|
||||
Three answers, and the middle one is the point: ``None`` on the packet means the
|
||||
@@ -145,7 +143,7 @@ def _device_mask(
|
||||
packet: SdkPacket,
|
||||
shape_hw: tuple[int, int],
|
||||
latch: Any = None,
|
||||
) -> tuple[Optional[np.ndarray], Optional[Diagnostic]]:
|
||||
) -> tuple[np.ndarray | None, Diagnostic | None]:
|
||||
"""The device's own validity, or a diagnostic when it describes another grid.
|
||||
|
||||
Both silent choices are wrong: stretching a mask from another grid marks samples
|
||||
@@ -177,7 +175,7 @@ def _device_mask(
|
||||
return array, None
|
||||
|
||||
|
||||
def _intersect(mask: np.ndarray, device: Optional[np.ndarray]) -> np.ndarray:
|
||||
def _intersect(mask: np.ndarray, device: np.ndarray | None) -> np.ndarray:
|
||||
"""Inside the UV island AND the device's own valid area, when it stated one."""
|
||||
if device is None:
|
||||
return freeze(mask, dtype=np.bool_)
|
||||
@@ -218,8 +216,8 @@ class FingerFrameAdapter:
|
||||
geometry_id: str,
|
||||
field_domain_id: str,
|
||||
shape_hw: tuple[int, int],
|
||||
map_x: Optional[np.ndarray] = None,
|
||||
map_y: Optional[np.ndarray] = None,
|
||||
map_x: np.ndarray | None = None,
|
||||
map_y: np.ndarray | None = None,
|
||||
preferred_field: str = "depth",
|
||||
) -> None:
|
||||
self.geometry_id = str(geometry_id)
|
||||
@@ -227,7 +225,7 @@ class FingerFrameAdapter:
|
||||
self.shape_hw = (int(shape_hw[0]), int(shape_hw[1]))
|
||||
self.map_x = map_x
|
||||
self.map_y = map_y
|
||||
self.last_diagnostic: Optional[Diagnostic] = None
|
||||
self.last_diagnostic: Diagnostic | None = None
|
||||
#: Which quantity the operator asked for (``"depth"`` or ``"flow"``). The other
|
||||
#: one is tried when this one cannot be coloured, and the substitution is stated
|
||||
#: rather than silent — a frame that shows another quantity than the setting says
|
||||
@@ -236,7 +234,7 @@ class FingerFrameAdapter:
|
||||
self.preferred_field = _field_name(preferred_field)
|
||||
#: Set (and cleared) by every snapshot: the reason the picture is not the field
|
||||
#: the operator chose. ``None`` when the preferred field was the one drawn.
|
||||
self.last_substitution: Optional[Diagnostic] = None
|
||||
self.last_substitution: Diagnostic | None = None
|
||||
#: The session's island latch (``image_backend.DomainMaskLatch``), when the
|
||||
#: window has one. See ``_device_mask``: the surface's validity is the *latched*
|
||||
#: island — the same mask the 2D canvas paints its island and its "no depth"
|
||||
@@ -247,7 +245,7 @@ class FingerFrameAdapter:
|
||||
self.preferred_field = _field_name(name)
|
||||
|
||||
@classmethod
|
||||
def from_domain(cls, domain: Any, *, geometry_id: str) -> "FingerFrameAdapter":
|
||||
def from_domain(cls, domain: Any, *, geometry_id: str) -> FingerFrameAdapter:
|
||||
"""Build from a ``LegacyFieldDomain`` (which carries the map, when loaded)."""
|
||||
return cls(
|
||||
geometry_id=geometry_id,
|
||||
@@ -259,7 +257,7 @@ class FingerFrameAdapter:
|
||||
|
||||
def snapshot(
|
||||
self, packet: SdkPacket, runtime: Any, identity: FrameIdentity
|
||||
) -> Optional[SurfaceFieldFrame]:
|
||||
) -> SurfaceFieldFrame | None:
|
||||
"""Colour the surface with the field the operator chose, or say why not.
|
||||
|
||||
Two quantities can be drawn and the operator picks one (``DisplaySettings.field``);
|
||||
@@ -306,7 +304,7 @@ class FingerFrameAdapter:
|
||||
identity: FrameIdentity,
|
||||
name: str,
|
||||
refused: list[Diagnostic],
|
||||
) -> Optional[SurfaceFieldFrame]:
|
||||
) -> SurfaceFieldFrame | None:
|
||||
"""One quantity, or ``None`` with the reason appended to ``refused``."""
|
||||
if name == "depth":
|
||||
depth = getattr(packet, "depth", None)
|
||||
@@ -401,7 +399,7 @@ class FingerFrameAdapter:
|
||||
mask: np.ndarray,
|
||||
descriptor: FieldDescriptor,
|
||||
refused: list[Diagnostic],
|
||||
) -> Optional[SurfaceFieldFrame]:
|
||||
) -> SurfaceFieldFrame | None:
|
||||
"""Intersect with the device's own mask and build the frame."""
|
||||
# Inside the UV island is the only place a value means anything: the tracker is
|
||||
# looking at masked black outside it.
|
||||
@@ -469,13 +467,13 @@ class BoxFrameAdapter:
|
||||
self.geometry_id = str(geometry_id)
|
||||
self.field_domain_id = str(field_domain_id)
|
||||
self.shape_hw = (int(shape_hw[0]), int(shape_hw[1]))
|
||||
self.last_diagnostic: Optional[Diagnostic] = None
|
||||
self.last_diagnostic: Diagnostic | None = None
|
||||
#: See ``FingerFrameAdapter.domain_latch``: one island, both views.
|
||||
self.domain_latch: Any = None
|
||||
|
||||
def snapshot(
|
||||
self, packet: SdkPacket, identity: FrameIdentity, domain: ExpectedGeometry
|
||||
) -> Optional[SurfaceFieldFrame]:
|
||||
) -> SurfaceFieldFrame | None:
|
||||
if tuple(domain.shape_hw) != self.shape_hw:
|
||||
self.last_diagnostic = diagnostic(
|
||||
"field_domain_mismatch",
|
||||
@@ -492,7 +490,7 @@ class BoxFrameAdapter:
|
||||
if mask_problem is not None:
|
||||
self.last_diagnostic = mask_problem
|
||||
return None
|
||||
refused: Optional[Diagnostic] = None
|
||||
refused: Diagnostic | None = None
|
||||
for name, field in candidates:
|
||||
if field is None:
|
||||
continue
|
||||
|
||||
@@ -28,12 +28,11 @@ exactly the kind of thing this feature is not allowed to cause.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from orisys_app_shared.paths import config_dir
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
@@ -43,6 +42,8 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
diagnostic,
|
||||
error,
|
||||
)
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from orisys_studio.scene3d.keys import SceneKeys, check_release_requirement
|
||||
|
||||
if TYPE_CHECKING: # the description is a value object; the callable returns one
|
||||
@@ -76,7 +77,7 @@ class SceneSource:
|
||||
return f"{self.kind}: {self.path}"
|
||||
|
||||
|
||||
def scene_dir(sensor_type: str, *, root: Optional[Path] = None) -> Path:
|
||||
def scene_dir(sensor_type: str, *, root: Path | None = None) -> Path:
|
||||
"""Where a compiled package for this sensor belongs."""
|
||||
base = Path(root) if root is not None else config_dir()
|
||||
return base / "sensors" / str(sensor_type) / "scene"
|
||||
@@ -86,8 +87,8 @@ def find_package(
|
||||
sensor_type: str,
|
||||
*,
|
||||
override: str | Path | None = None,
|
||||
root: Optional[Path] = None,
|
||||
) -> Optional[SceneSource]:
|
||||
root: Path | None = None,
|
||||
) -> SceneSource | None:
|
||||
"""The package to load, or ``None``. Never raises: absence is a state."""
|
||||
if override:
|
||||
path = Path(str(override)).expanduser()
|
||||
@@ -126,8 +127,8 @@ def no_asset_diagnostic(sensor_type: str, *, override: str | Path | None = None)
|
||||
|
||||
def load_package(
|
||||
source: SceneSource,
|
||||
expected: Optional[ExpectedGeometry] = None,
|
||||
keys: "Optional[SceneKeys]" = None,
|
||||
expected: ExpectedGeometry | None = None,
|
||||
keys: SceneKeys | None = None,
|
||||
) -> SceneAssets:
|
||||
"""Read and validate a package. Runs on a worker thread; raises ``ThreeDError``.
|
||||
|
||||
@@ -153,10 +154,10 @@ def load_package(
|
||||
|
||||
def _resolve_and_load(
|
||||
source: SceneSource,
|
||||
expected: Optional[ExpectedGeometry],
|
||||
provider: "Optional[Callable[[], DeviceGeometry]]",
|
||||
keys: "Optional[SceneKeys]" = None,
|
||||
) -> tuple[SceneAssets, "Optional[DeviceGeometry]"]:
|
||||
expected: ExpectedGeometry | None,
|
||||
provider: Callable[[], DeviceGeometry] | None,
|
||||
keys: SceneKeys | None = None,
|
||||
) -> tuple[SceneAssets, DeviceGeometry | None]:
|
||||
"""Worker-thread half: build the *device's* description, then read the package.
|
||||
|
||||
Deriving the description digests the unroll map the runtime loads, so it belongs on
|
||||
@@ -183,21 +184,21 @@ class SceneAssetLoader(QObject):
|
||||
loaded = Signal(object, str, int, object) # SceneAssets, description, id, device
|
||||
failed = Signal(object, int) # Diagnostic, request id
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._executor: Optional[ThreadPoolExecutor] = None
|
||||
self._executor: ThreadPoolExecutor | None = None
|
||||
self._inflight = False
|
||||
self._request_id = 0
|
||||
#: The newest request that could not start yet (source, expected, provider, id).
|
||||
self._pending: Optional[tuple[SceneSource, Optional[ExpectedGeometry], Any, int]] = None
|
||||
self._pending: tuple[SceneSource, ExpectedGeometry | None, Any, int] | None = None
|
||||
|
||||
def load(
|
||||
self,
|
||||
source: SceneSource,
|
||||
expected: Optional[ExpectedGeometry] = None,
|
||||
expected: ExpectedGeometry | None = None,
|
||||
*,
|
||||
expected_provider: "Optional[Callable[[], DeviceGeometry]]" = None,
|
||||
keys: "Optional[SceneKeys]" = None,
|
||||
expected_provider: Callable[[], DeviceGeometry] | None = None,
|
||||
keys: SceneKeys | None = None,
|
||||
) -> int:
|
||||
"""Ask for a scene; returns the request id this call was given."""
|
||||
self._request_id += 1
|
||||
|
||||
@@ -15,14 +15,12 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
F32,
|
||||
CameraMatrices,
|
||||
CameraPreset,
|
||||
F32,
|
||||
freeze,
|
||||
)
|
||||
|
||||
@@ -54,7 +52,7 @@ class OrbitCamera:
|
||||
vertical_fov_rad: float = 0.6
|
||||
aspect: float = 1.0
|
||||
_fitted_radius_m: float = 0.05
|
||||
_preset_name: Optional[str] = None
|
||||
_preset_name: str | None = None
|
||||
|
||||
# ------------------------------------------------------------------- setup
|
||||
def fit(self, bounds_sensor_m: F32, aspect: float) -> None:
|
||||
|
||||
@@ -36,9 +36,10 @@ from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import CancelledError, Executor, Future
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Optional
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
Diagnostic,
|
||||
@@ -106,27 +107,27 @@ class ThreeDCoordinator:
|
||||
#: withdrawn: a stop, a hide, a new session, a new scene. A compose result
|
||||
#: whose epoch is not this one is thrown away instead of presented.
|
||||
self._epoch = 0
|
||||
self._assets: Optional[SceneAssets] = None
|
||||
self._assets: SceneAssets | None = None
|
||||
self._source_id = ""
|
||||
self._session_id: Optional[int] = None
|
||||
self._session_id: int | None = None
|
||||
#: The newest request id seen in this epoch: a producer that falls behind
|
||||
#: (a seek UI, a queue that reorders) must not put an older frame on screen.
|
||||
self._last_request_id = 0
|
||||
self._visible = False
|
||||
self._settings = DisplaySettings(max_fps=self._max_fps)
|
||||
self._inflight: Optional[_Inflight] = None
|
||||
self._inflight: _Inflight | None = None
|
||||
self._stale_frames = 0
|
||||
self._invalid_frames = 0
|
||||
self._last_diagnostic: Optional[Diagnostic] = None
|
||||
self._last_diagnostic: Diagnostic | None = None
|
||||
self._closed = False
|
||||
self._settings_dirty = False
|
||||
self._last_composed_identity: Optional[FrameIdentity] = None
|
||||
self._last_composed_identity: FrameIdentity | None = None
|
||||
#: What is on screen and when it got there, for the pane's frame counter.
|
||||
self._last_presented: Optional[tuple[FrameIdentity, float]] = None
|
||||
self._last_presented: tuple[FrameIdentity, float] | None = None
|
||||
#: When the current measurement stopped being current, or ``None`` while the
|
||||
#: view is live. Set by a field that could not be built, a compose that
|
||||
#: failed, or a frame whose array belongs to an earlier acquisition frame.
|
||||
self._stale_since: Optional[float] = None
|
||||
self._stale_since: float | None = None
|
||||
#: Whether the reference model is already on screen for the current claim.
|
||||
#: Without this the pane re-uploaded the same rest frame on every 16 ms tick
|
||||
#: while it waited for the first measurement.
|
||||
@@ -138,7 +139,7 @@ class ThreeDCoordinator:
|
||||
self._last_result_latency_ms = 0.0
|
||||
#: The field frame currently on screen, so a gain change can redraw exactly
|
||||
#: it rather than a newer instant.
|
||||
self._last_field_frame: Optional[SurfaceFieldFrame] = None
|
||||
self._last_field_frame: SurfaceFieldFrame | None = None
|
||||
|
||||
# ------------------------------------------------------------------- state
|
||||
@property
|
||||
@@ -155,7 +156,7 @@ class ThreeDCoordinator:
|
||||
"""The display claim results are accepted under (see :class:`_Inflight`)."""
|
||||
return self._epoch
|
||||
|
||||
def last_presented(self) -> Optional[tuple[FrameIdentity, float]]:
|
||||
def last_presented(self) -> tuple[FrameIdentity, float] | None:
|
||||
"""What is on screen, and the monotonic time it was handed to the widget."""
|
||||
return self._last_presented
|
||||
|
||||
@@ -190,7 +191,7 @@ class ThreeDCoordinator:
|
||||
return self._invalid_frames
|
||||
|
||||
@property
|
||||
def last_diagnostic(self) -> Optional[Diagnostic]:
|
||||
def last_diagnostic(self) -> Diagnostic | None:
|
||||
return self._last_diagnostic
|
||||
|
||||
@property
|
||||
@@ -355,7 +356,7 @@ class ThreeDCoordinator:
|
||||
self._last_request_id = max(self._last_request_id, identity.request_id)
|
||||
return self._incoming.offer(frame)
|
||||
|
||||
def note_field_failure(self, item: Optional[Diagnostic] = None) -> None:
|
||||
def note_field_failure(self, item: Diagnostic | None = None) -> None:
|
||||
"""A producer could not build a field frame from this packet.
|
||||
|
||||
Called from the acquisition thread by the sink that owns the adapter: when the
|
||||
@@ -373,7 +374,7 @@ class ThreeDCoordinator:
|
||||
@staticmethod
|
||||
def _reject_reason(
|
||||
frame: SurfaceFieldFrame, assets: SceneAssets
|
||||
) -> Optional[Diagnostic]:
|
||||
) -> Diagnostic | None:
|
||||
"""The cheap checks a producer thread may do: identity and shape only."""
|
||||
if frame.geometry_id != assets.geometry_id:
|
||||
return diagnostic(
|
||||
|
||||
@@ -24,14 +24,15 @@ zero: the reference model is not a measurement and must not look like one.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
CameraMatrices,
|
||||
Diagnostic,
|
||||
DisplaySettings,
|
||||
GLCapabilities,
|
||||
MeshPart,
|
||||
@@ -43,7 +44,8 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
error,
|
||||
freeze,
|
||||
)
|
||||
from orisys_app_shared.scene3d.contracts import Diagnostic
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Renderer names that are not a GPU. A board must not be signed off on one of
|
||||
#: these, and neither must a laptop with a broken driver.
|
||||
@@ -261,7 +263,7 @@ def shader_source(profile: Profile, program: str) -> tuple[str, str]:
|
||||
return header + vertex, header + body
|
||||
|
||||
|
||||
def _valid_scalar_range(scalar, valid) -> Optional[tuple[float, float]]:
|
||||
def _valid_scalar_range(scalar, valid) -> tuple[float, float] | None:
|
||||
"""Robust P5/P95 range of *valid* samples, or ``None`` when there are none.
|
||||
|
||||
Using min/max made empty-field noise paint the whole LUT. A percentile span
|
||||
@@ -294,11 +296,11 @@ def _valid_scalar_range(scalar, valid) -> Optional[tuple[float, float]]:
|
||||
|
||||
|
||||
def _blend_scale_range(
|
||||
previous: Optional[tuple[float, float]],
|
||||
current: Optional[tuple[float, float]],
|
||||
previous: tuple[float, float] | None,
|
||||
current: tuple[float, float] | None,
|
||||
*,
|
||||
alpha: float = 0.15,
|
||||
) -> Optional[tuple[float, float]]:
|
||||
) -> tuple[float, float] | None:
|
||||
"""Slowly update an automatic colour range to avoid frame-to-frame flicker."""
|
||||
if current is None:
|
||||
return previous
|
||||
@@ -310,7 +312,7 @@ def _blend_scale_range(
|
||||
(1.0 - a) * float(previous[1]) + a * float(current[1]),
|
||||
)
|
||||
|
||||
def _scale_bounds(scale, automatic: Optional[tuple[float, float]]) -> tuple[float, float]:
|
||||
def _scale_bounds(scale, automatic: tuple[float, float] | None) -> tuple[float, float]:
|
||||
"""The colour range to draw with: the operator's, or the data's own.
|
||||
|
||||
``automatic`` mode exists because the quantities differ in magnitude by orders
|
||||
@@ -327,7 +329,7 @@ def _scale_bounds(scale, automatic: Optional[tuple[float, float]]) -> tuple[floa
|
||||
else:
|
||||
fallback = (float(scale.lower), float(scale.upper))
|
||||
lower, upper = float(fallback[0]), float(fallback[1])
|
||||
if not (lower == lower) or not (upper == upper): # NaN
|
||||
if math.isnan(lower) or math.isnan(upper):
|
||||
return 0.0, 1.0
|
||||
if upper <= lower:
|
||||
# A constant field still has to be visible rather than all one clipped colour.
|
||||
@@ -404,7 +406,7 @@ def _extend_edges(values: np.ndarray, usable: np.ndarray) -> np.ndarray:
|
||||
return np.where(usable, values, filled)
|
||||
|
||||
|
||||
def pack_part(part: MeshPart, *, uvs: Optional[np.ndarray] = None) -> np.ndarray:
|
||||
def pack_part(part: MeshPart, *, uvs: np.ndarray | None = None) -> np.ndarray:
|
||||
"""Interleave one part into the layout the shaders read.
|
||||
|
||||
``uvs`` are the render-vertex UVs on the domain's pixel grid
|
||||
@@ -431,7 +433,7 @@ def pack_part(part: MeshPart, *, uvs: Optional[np.ndarray] = None) -> np.ndarray
|
||||
|
||||
|
||||
def pack_scene_part(
|
||||
part: MeshPart, *, uvs: Optional[np.ndarray] = None
|
||||
part: MeshPart, *, uvs: np.ndarray | None = None
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""``(vertices, indices)`` as the GPU wants them, both read-only."""
|
||||
indices = np.asarray(part.triangles, dtype=np.uint32).reshape(-1)
|
||||
@@ -598,7 +600,14 @@ class _PartBuffers:
|
||||
"""One part's GPU state. Kept dumb on purpose: no Qt, no ownership games."""
|
||||
|
||||
__slots__ = (
|
||||
"part_id", "role", "vao", "vbo", "ebo", "triangles", "vertices", "rows"
|
||||
"ebo",
|
||||
"part_id",
|
||||
"role",
|
||||
"rows",
|
||||
"triangles",
|
||||
"vao",
|
||||
"vbo",
|
||||
"vertices"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -620,7 +629,7 @@ class GLRenderer:
|
||||
"""Shaders, buffers and draw calls for one scene."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._capabilities: Optional[GLCapabilities] = None
|
||||
self._capabilities: GLCapabilities | None = None
|
||||
self._programs: dict[str, int] = {}
|
||||
self._uniforms: dict[str, dict[str, int]] = {}
|
||||
self._parts: dict[str, _PartBuffers] = {}
|
||||
@@ -632,13 +641,13 @@ class GLRenderer:
|
||||
#: allocation and no per-frame vertex traffic.
|
||||
self._field_texture: int = 0
|
||||
self._valid_texture: int = 0
|
||||
self._field_shape: Optional[tuple[int, int]] = None
|
||||
self._field_shape: tuple[int, int] | None = None
|
||||
self._field_internal_format: int = 0
|
||||
#: Range of the valid samples of the field currently uploaded, for the
|
||||
#: automatic colour scale. ``None`` until a frame carries valid data.
|
||||
self._scalar_range: Optional[tuple[float, float]] = None
|
||||
self._scalar_range: tuple[float, float] | None = None
|
||||
#: The colour bounds of the last draw and how they were chosen.
|
||||
self._last_scale: Optional[tuple[float, float]] = None
|
||||
self._last_scale: tuple[float, float] | None = None
|
||||
self._last_scale_mode = ""
|
||||
self._sensing_part_id: str = ""
|
||||
self._estimated_gpu_bytes: int = 0
|
||||
@@ -926,8 +935,12 @@ class GLRenderer:
|
||||
GL.glDeleteVertexArrays(1, [int(vao)])
|
||||
GL.glDeleteBuffers(1, [int(vbo)])
|
||||
GL.glDeleteBuffers(1, [int(ebo)])
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
except Exception as exc: # noqa: BLE001 - best-effort delete, reported below
|
||||
# A context that has already been lost refuses the delete, and this also
|
||||
# runs on the live overlay-replacement path: raising here would replace a
|
||||
# frame with a crash. Reported rather than swallowed, because a delete
|
||||
# that keeps failing leaks GPU memory for the rest of the session.
|
||||
logger.warning("could not free the %s overlay's GL objects: %s", kind, exc)
|
||||
|
||||
def draw(
|
||||
self,
|
||||
@@ -1021,7 +1034,7 @@ class GLRenderer:
|
||||
scale_mode=self._last_scale_mode,
|
||||
)
|
||||
|
||||
def effective_scale(self) -> Optional[tuple[float, float]]:
|
||||
def effective_scale(self) -> tuple[float, float] | None:
|
||||
"""The colour bounds the last draw actually used (``None`` before any draw)."""
|
||||
return self._last_scale
|
||||
|
||||
@@ -1059,7 +1072,7 @@ class GLRenderer:
|
||||
GL.glActiveTexture(GL.GL_TEXTURE0)
|
||||
|
||||
@property
|
||||
def field_shape(self) -> Optional[tuple[int, int]]:
|
||||
def field_shape(self) -> tuple[int, int] | None:
|
||||
"""The allocated field texture's ``(H, W)``, or ``None`` before the first frame."""
|
||||
return self._field_shape
|
||||
|
||||
@@ -1150,7 +1163,7 @@ class GLRenderer:
|
||||
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
|
||||
self._lut_id = lut_id
|
||||
|
||||
def capabilities(self) -> Optional[GLCapabilities]:
|
||||
def capabilities(self) -> GLCapabilities | None:
|
||||
return self._capabilities
|
||||
|
||||
@property
|
||||
@@ -1246,7 +1259,7 @@ def _decode(value: Any) -> str:
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
def _set_mat4(GL: Any, location: Optional[int], values: np.ndarray) -> None:
|
||||
def _set_mat4(GL: Any, location: int | None, values: np.ndarray) -> None:
|
||||
if location is None:
|
||||
return
|
||||
# Column-major upload, explicitly: the layout of the NumPy array must not
|
||||
@@ -1254,17 +1267,17 @@ def _set_mat4(GL: Any, location: Optional[int], values: np.ndarray) -> None:
|
||||
GL.glUniformMatrix4fv(location, 1, GL.GL_FALSE, np.ascontiguousarray(values.T))
|
||||
|
||||
|
||||
def _set_float(GL: Any, location: Optional[int], value: float) -> None:
|
||||
def _set_float(GL: Any, location: int | None, value: float) -> None:
|
||||
if location is not None:
|
||||
GL.glUniform1f(location, float(value))
|
||||
|
||||
|
||||
def _set_vec3(GL: Any, location: Optional[int], values: tuple[float, float, float]) -> None:
|
||||
def _set_vec3(GL: Any, location: int | None, values: tuple[float, float, float]) -> None:
|
||||
if location is not None:
|
||||
GL.glUniform3f(location, float(values[0]), float(values[1]), float(values[2]))
|
||||
|
||||
|
||||
def _set_vec4(GL: Any, location: Optional[int], values: tuple[float, float, float, float]) -> None:
|
||||
def _set_vec4(GL: Any, location: int | None, values: tuple[float, float, float, float]) -> None:
|
||||
if location is not None:
|
||||
GL.glUniform4f(
|
||||
location, float(values[0]), float(values[1]), float(values[2]), float(values[3])
|
||||
|
||||
@@ -16,9 +16,10 @@ paths and booleans so a recording can say how a package was authenticated.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Optional
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import Diagnostic, diagnostic
|
||||
|
||||
@@ -36,7 +37,7 @@ PREF_REQUIRE_SIGNATURE = "scene3d_require_signature"
|
||||
class SceneKeys:
|
||||
"""What the loader needs to authenticate a package — and how strict to be."""
|
||||
|
||||
verify_key_pem: Optional[bytes] = None
|
||||
verify_key_pem: bytes | None = None
|
||||
provider: Any = None
|
||||
require_signature: bool = False
|
||||
#: Paths/ids only, for logs and for the recording: never material.
|
||||
@@ -59,7 +60,7 @@ class SceneKeys:
|
||||
}
|
||||
|
||||
|
||||
def resolve_scene_keys(prefs: Optional[Mapping[str, Any]]) -> SceneKeys:
|
||||
def resolve_scene_keys(prefs: Mapping[str, Any] | None) -> SceneKeys:
|
||||
"""Build the key material the preferences describe. Never raises for absence.
|
||||
|
||||
A missing or unreadable file is a *diagnostic-bearing state*, not a crash: the 2D
|
||||
@@ -68,7 +69,7 @@ def resolve_scene_keys(prefs: Optional[Mapping[str, Any]]) -> SceneKeys:
|
||||
"""
|
||||
prefs = dict(prefs or {})
|
||||
sources: dict[str, str] = {}
|
||||
verify_pem: Optional[bytes] = None
|
||||
verify_pem: bytes | None = None
|
||||
provider = None
|
||||
|
||||
verify_path = str(prefs.get(PREF_VERIFY_KEY) or "").strip()
|
||||
@@ -113,7 +114,7 @@ def resolve_scene_keys(prefs: Optional[Mapping[str, Any]]) -> SceneKeys:
|
||||
)
|
||||
|
||||
|
||||
def check_release_requirement(path: Path, keys: Optional[SceneKeys]) -> Optional[Diagnostic]:
|
||||
def check_release_requirement(path: Path, keys: SceneKeys | None) -> Diagnostic | None:
|
||||
"""Refuse an unsigned package when the application is in release mode.
|
||||
|
||||
Development builds accept a plain package (the shipped engineering one is exactly
|
||||
|
||||
@@ -18,10 +18,20 @@ Two rules it exists to enforce in the interface:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
ColorScale,
|
||||
Diagnostic,
|
||||
DisplaySettings,
|
||||
FieldDescriptor,
|
||||
RenderStats,
|
||||
SceneAssets,
|
||||
SceneCapabilities,
|
||||
SceneState,
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QImage
|
||||
from PySide6.QtWidgets import (
|
||||
@@ -38,18 +48,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
ColorScale,
|
||||
Diagnostic,
|
||||
DisplaySettings,
|
||||
FieldDescriptor,
|
||||
RenderStats,
|
||||
SceneAssets,
|
||||
SceneCapabilities,
|
||||
SceneState,
|
||||
)
|
||||
from orisys_studio.scene3d.prefs import FPS_CHOICES, DEFAULT_FIELD
|
||||
from orisys_studio.scene3d.prefs import DEFAULT_FIELD, FPS_CHOICES
|
||||
from orisys_studio.ui.fit_text import ShrinkableLabel
|
||||
|
||||
if TYPE_CHECKING: # the panel talks to these two by interface, not by import
|
||||
@@ -82,7 +81,7 @@ class ColorScaleBar(QLabel):
|
||||
because in automatic mode the spin boxes beside it held values nobody was using.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("colorScaleBar")
|
||||
self.setFixedHeight(12)
|
||||
@@ -136,23 +135,23 @@ class ThreeDPanel(QFrame):
|
||||
self,
|
||||
coordinator: ThreeDCoordinator,
|
||||
widget: Sensor3DWidget,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("contentPanel")
|
||||
self._coordinator = coordinator
|
||||
self._widget = widget
|
||||
self._assets: Optional[SceneAssets] = None
|
||||
self._descriptor: Optional[FieldDescriptor] = None
|
||||
self._assets: SceneAssets | None = None
|
||||
self._descriptor: FieldDescriptor | None = None
|
||||
self._state: SceneState = "disabled"
|
||||
self._source_kind = "live"
|
||||
self._syncing = False
|
||||
#: What the data source can provide (see ``contracts.SceneCapabilities``): an
|
||||
#: unavailable capability disables its control and says why, rather than
|
||||
#: letting the operator operate an input that does not exist.
|
||||
self._capabilities: Optional[SceneCapabilities] = None
|
||||
self._capabilities: SceneCapabilities | None = None
|
||||
#: The bounds the last draw actually used (the widget's RenderStats).
|
||||
self._effective_scale: Optional[RenderStats] = None
|
||||
self._effective_scale: RenderStats | None = None
|
||||
#: Whether the pane explains the pipeline around the picture (the window's
|
||||
#: 「诊断信息」 switch). Off by default: the picture and what it *is* stay.
|
||||
self._show_diagnostics = False
|
||||
@@ -355,7 +354,7 @@ class ThreeDPanel(QFrame):
|
||||
self.set_state("disabled")
|
||||
|
||||
# ------------------------------------------------------------------ inputs
|
||||
def set_scene(self, assets: Optional[SceneAssets]) -> None:
|
||||
def set_scene(self, assets: SceneAssets | None) -> None:
|
||||
"""Fill the preset list from the asset: its default view is the asset's."""
|
||||
self._assets = assets
|
||||
self._syncing = True
|
||||
@@ -377,7 +376,7 @@ class ThreeDPanel(QFrame):
|
||||
self.preset_combo.setEnabled(bool(assets is not None and self.preset_combo.count()))
|
||||
self.reset_button.setEnabled(assets is not None)
|
||||
|
||||
def set_descriptor(self, descriptor: Optional[FieldDescriptor]) -> None:
|
||||
def set_descriptor(self, descriptor: FieldDescriptor | None) -> None:
|
||||
"""The measurement currently on screen — or none, which hides the bar."""
|
||||
if descriptor != self._descriptor:
|
||||
self._effective_scale = None
|
||||
@@ -417,7 +416,7 @@ class ThreeDPanel(QFrame):
|
||||
"""
|
||||
self.scale_bar.set_ramp(ramp)
|
||||
|
||||
def set_capabilities(self, capabilities: Optional[SceneCapabilities]) -> None:
|
||||
def set_capabilities(self, capabilities: SceneCapabilities | None) -> None:
|
||||
"""Offer only the controls this data source can actually feed.
|
||||
|
||||
Today's adapters colour a scalar in place and publish no displacement at all, so
|
||||
@@ -451,7 +450,7 @@ class ThreeDPanel(QFrame):
|
||||
else t("scene3d.contact_arrow_tip")
|
||||
)
|
||||
|
||||
def capabilities(self) -> Optional[SceneCapabilities]:
|
||||
def capabilities(self) -> SceneCapabilities | None:
|
||||
return self._capabilities
|
||||
|
||||
def _render_legend(self) -> None:
|
||||
@@ -551,7 +550,7 @@ class ThreeDPanel(QFrame):
|
||||
self.source_label.setToolTip(t(f"scene3d.source_{kind}_tip"))
|
||||
self.source_label.setVisible(self._show_diagnostics)
|
||||
|
||||
def set_diagnostic(self, diagnostic: Optional[Diagnostic]) -> None:
|
||||
def set_diagnostic(self, diagnostic: Diagnostic | None) -> None:
|
||||
"""One diagnostic line. The 2D views stay usable behind it.
|
||||
|
||||
Warnings and errors are always stated: they are claims about the *measurement*
|
||||
|
||||
@@ -19,13 +19,15 @@ file cannot relabel a measurement as another quantity.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping, Optional
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
UNAVAILABLE_DESCRIPTOR,
|
||||
ColorScale,
|
||||
DisplaySettings,
|
||||
FieldDescriptor,
|
||||
UNAVAILABLE_DESCRIPTOR,
|
||||
)
|
||||
|
||||
#: Frame rates the pane offers. A saved value outside this set is repaired to the
|
||||
@@ -51,7 +53,7 @@ def _number(value: Any, fallback: float) -> float:
|
||||
number = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return fallback
|
||||
if number != number or number in (float("inf"), float("-inf")): # NaN / inf
|
||||
if math.isnan(number) or number in (float("inf"), float("-inf")): # NaN / inf
|
||||
return fallback
|
||||
return number
|
||||
|
||||
@@ -102,9 +104,9 @@ def settings_to_prefs(settings: DisplaySettings) -> dict[str, Any]:
|
||||
|
||||
|
||||
def settings_from_prefs(
|
||||
raw: Optional[Mapping[str, Any]],
|
||||
raw: Mapping[str, Any] | None,
|
||||
*,
|
||||
descriptor: Optional[FieldDescriptor] = None,
|
||||
descriptor: FieldDescriptor | None = None,
|
||||
) -> DisplaySettings:
|
||||
"""Rebuild the display settings, repairing anything unusable.
|
||||
|
||||
@@ -121,7 +123,7 @@ def settings_from_prefs(
|
||||
field = str(raw.get("field") or DEFAULT_FIELD)
|
||||
if field not in FIELD_CHOICES:
|
||||
field = DEFAULT_FIELD
|
||||
scale: Optional[ColorScale] = None
|
||||
scale: ColorScale | None = None
|
||||
saved = raw.get("color_scale")
|
||||
if isinstance(saved, Mapping):
|
||||
mode = str(saved.get("mode") or "fixed")
|
||||
|
||||
@@ -14,8 +14,9 @@ widgets — and one set of signals — whether or not the dialog has ever been o
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
@@ -25,8 +26,6 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
if TYPE_CHECKING: # the panel is passed in; only its interfaces are used here
|
||||
from orisys_studio.scene3d.panel import ThreeDPanel
|
||||
|
||||
@@ -35,7 +34,7 @@ class Scene3dSettingsDialog(QDialog):
|
||||
"""A modeless home for the 3D pane's controls."""
|
||||
|
||||
def __init__(
|
||||
self, panel: "ThreeDPanel", parent: Optional[QWidget] = None
|
||||
self, panel: ThreeDPanel, parent: QWidget | None = None
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("scene3dSettings")
|
||||
|
||||
@@ -25,11 +25,9 @@ description disagrees with the asset is refused with both shapes named.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.scene3d.composer import SceneComposer
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
@@ -47,6 +45,9 @@ from orisys_app_shared.scene3d.contracts import (
|
||||
diagnostic,
|
||||
validate_assets,
|
||||
)
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from orisys_studio.scene3d.adapters import BoxFrameAdapter, FingerFrameAdapter
|
||||
from orisys_studio.scene3d.camera import OrbitCamera
|
||||
from orisys_studio.scene3d.coordinator import ThreeDCoordinator
|
||||
@@ -90,24 +91,24 @@ class Scene3DView(QWidget):
|
||||
descriptorChanged = Signal(object) # Optional[FieldDescriptor]
|
||||
enabledChanged = Signal(bool)
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._available, self._reason = gl_available()
|
||||
self._widget: Optional[Sensor3DWidget] = None
|
||||
self._panel: Optional[ThreeDPanel] = None
|
||||
self._coordinator: Optional[ThreeDCoordinator] = None
|
||||
self._widget: Sensor3DWidget | None = None
|
||||
self._panel: ThreeDPanel | None = None
|
||||
self._coordinator: ThreeDCoordinator | None = None
|
||||
self._camera = OrbitCamera()
|
||||
self._executor: Optional[ThreadPoolExecutor] = None
|
||||
self._assets: Optional[SceneAssets] = None
|
||||
self._composer: Optional[SceneComposer] = None
|
||||
self._executor: ThreadPoolExecutor | None = None
|
||||
self._assets: SceneAssets | None = None
|
||||
self._composer: SceneComposer | None = None
|
||||
#: The session's island latch, shared with the 2D canvas when the window hands
|
||||
#: one over (``image_backend.DomainMaskLatch``). One latch, two views: the
|
||||
#: surface's validity texture and the canvas's island are the same mask, so
|
||||
#: they cannot drift apart. ``None`` means "no session": the adapter then uses
|
||||
#: each frame's own mask, which is what the tests and the PoC want.
|
||||
self._domain_latch: Any = None
|
||||
self._expected: Optional[ExpectedGeometry] = None
|
||||
self._snapshot: Optional[Callable[[Any, FrameIdentity], Any]] = None
|
||||
self._expected: ExpectedGeometry | None = None
|
||||
self._snapshot: Callable[[Any, FrameIdentity], Any] | None = None
|
||||
#: The adapter behind ``_snapshot``: it owns the reason a packet was refused,
|
||||
#: and (for a source that cannot provide a displacement) what the pane may
|
||||
#: offer. Kept rather than closed over so those answers can be asked.
|
||||
@@ -115,11 +116,11 @@ class Scene3DView(QWidget):
|
||||
self._source_id = ""
|
||||
self._enabled = False
|
||||
self._source_kind = "live"
|
||||
self._last_diagnostic: Optional[Diagnostic] = None
|
||||
self._last_descriptor: Optional[FieldDescriptor] = None
|
||||
self._last_stats: Optional[RenderStats] = None
|
||||
self._last_diagnostic: Diagnostic | None = None
|
||||
self._last_descriptor: FieldDescriptor | None = None
|
||||
self._last_stats: RenderStats | None = None
|
||||
#: The coordinator's last diagnostic as *this* view has already reported it.
|
||||
self._reported_diagnostic: Optional[Diagnostic] = None
|
||||
self._reported_diagnostic: Diagnostic | None = None
|
||||
#: Whether the loaded asset was compared against the device's own description.
|
||||
self._device_verified = False
|
||||
#: Frames the producer did not even build (a frame was already waiting).
|
||||
@@ -173,7 +174,7 @@ class Scene3DView(QWidget):
|
||||
self,
|
||||
assets: SceneAssets,
|
||||
source_id: str,
|
||||
expected: Optional[ExpectedGeometry] = None,
|
||||
expected: ExpectedGeometry | None = None,
|
||||
) -> bool:
|
||||
"""Adopt a validated scene. Returns whether the view accepted it.
|
||||
|
||||
@@ -242,8 +243,8 @@ class Scene3DView(QWidget):
|
||||
return not violations
|
||||
|
||||
def _make_snapshot(
|
||||
self, assets: SceneAssets, expected: Optional[ExpectedGeometry]
|
||||
) -> Optional[Callable[[Any, FrameIdentity], Any]]:
|
||||
self, assets: SceneAssets, expected: ExpectedGeometry | None
|
||||
) -> Callable[[Any, FrameIdentity], Any] | None:
|
||||
"""Bind the adapter that fits this scene, or refuse with a diagnostic.
|
||||
|
||||
The two adapters have different call signatures because they need different
|
||||
@@ -358,12 +359,12 @@ class Scene3DView(QWidget):
|
||||
except Exception: # noqa: BLE001 - a display sink must never break capture
|
||||
logger.exception("the 3D sink failed for one packet")
|
||||
|
||||
def _snapshot_diagnostic(self) -> Optional[Diagnostic]:
|
||||
def _snapshot_diagnostic(self) -> Diagnostic | None:
|
||||
"""Why the adapter refused the last packet (the adapter owns the answer)."""
|
||||
item = getattr(self._adapter, "last_diagnostic", None)
|
||||
return item if isinstance(item, Diagnostic) else None
|
||||
|
||||
def identity_for(self, packet: Any) -> Optional[FrameIdentity]:
|
||||
def identity_for(self, packet: Any) -> FrameIdentity | None:
|
||||
"""The identity a field frame built from this packet would carry."""
|
||||
if self._coordinator is None or self._assets is None:
|
||||
return None
|
||||
@@ -448,7 +449,7 @@ class Scene3DView(QWidget):
|
||||
self._panel.set_source_kind(kind)
|
||||
self._publish_capabilities()
|
||||
|
||||
def capabilities(self) -> Optional[SceneCapabilities]:
|
||||
def capabilities(self) -> SceneCapabilities | None:
|
||||
"""What this data source can provide, for the panel's controls.
|
||||
|
||||
Read off the adapter that is actually bound: an adapter that colours a scalar in
|
||||
@@ -538,27 +539,27 @@ class Scene3DView(QWidget):
|
||||
return self._enabled
|
||||
|
||||
@property
|
||||
def assets(self) -> Optional[SceneAssets]:
|
||||
def assets(self) -> SceneAssets | None:
|
||||
return self._assets
|
||||
|
||||
@property
|
||||
def coordinator(self) -> Optional[ThreeDCoordinator]:
|
||||
def coordinator(self) -> ThreeDCoordinator | None:
|
||||
return self._coordinator
|
||||
|
||||
@property
|
||||
def gl_widget(self) -> Optional[Sensor3DWidget]:
|
||||
def gl_widget(self) -> Sensor3DWidget | None:
|
||||
return self._widget
|
||||
|
||||
@property
|
||||
def panel(self) -> Optional[ThreeDPanel]:
|
||||
def panel(self) -> ThreeDPanel | None:
|
||||
return self._panel
|
||||
|
||||
@property
|
||||
def last_diagnostic(self) -> Optional[Diagnostic]:
|
||||
def last_diagnostic(self) -> Diagnostic | None:
|
||||
return self._last_diagnostic
|
||||
|
||||
@property
|
||||
def last_descriptor(self) -> Optional[FieldDescriptor]:
|
||||
def last_descriptor(self) -> FieldDescriptor | None:
|
||||
return self._last_descriptor
|
||||
|
||||
def state(self) -> SceneState:
|
||||
@@ -568,7 +569,7 @@ class Scene3DView(QWidget):
|
||||
return "disabled"
|
||||
return self._coordinator.state
|
||||
|
||||
def render_stats(self) -> Optional[RenderStats]:
|
||||
def render_stats(self) -> RenderStats | None:
|
||||
return self._last_stats
|
||||
|
||||
def describe(self) -> dict:
|
||||
@@ -751,7 +752,7 @@ class Scene3DView(QWidget):
|
||||
self._panel.set_state(state)
|
||||
self.stateChanged.emit(state)
|
||||
|
||||
def _publish_coordinator_diagnostic(self, item: Optional[Diagnostic]) -> None:
|
||||
def _publish_coordinator_diagnostic(self, item: Diagnostic | None) -> None:
|
||||
"""Raise a coordinator diagnostic once, when it changes."""
|
||||
if item is None or item == self._reported_diagnostic:
|
||||
return
|
||||
|
||||
@@ -17,23 +17,24 @@ two-dimensional application carries on.
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import QEvent, Qt, Signal
|
||||
from PySide6.QtGui import QMouseEvent, QWheelEvent
|
||||
# Qt6 moved QOpenGLWidget out of QtWidgets into its own module; PySide6 follows.
|
||||
from PySide6.QtOpenGLWidgets import QOpenGLWidget
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
Diagnostic,
|
||||
DisplaySettings,
|
||||
FrameIdentity,
|
||||
GLCapabilities,
|
||||
RenderFrame,
|
||||
RenderStats,
|
||||
SceneAssets,
|
||||
diagnostic,
|
||||
)
|
||||
from PySide6.QtCore import QEvent, Qt, Signal
|
||||
from PySide6.QtGui import QMouseEvent, QWheelEvent
|
||||
|
||||
# Qt6 moved QOpenGLWidget out of QtWidgets into its own module; PySide6 follows.
|
||||
from PySide6.QtOpenGLWidgets import QOpenGLWidget
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from orisys_studio.scene3d.camera import OrbitCamera
|
||||
from orisys_studio.scene3d.gl_backend import (
|
||||
GLDispatch,
|
||||
@@ -62,27 +63,27 @@ class Sensor3DWidget(QOpenGLWidget):
|
||||
self,
|
||||
renderer: GLRenderer,
|
||||
camera: OrbitCamera,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent: QWidget | None = None,
|
||||
*,
|
||||
dispatch: Optional[GLDispatch] = None,
|
||||
dispatch: GLDispatch | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self._renderer = renderer
|
||||
self._camera = camera
|
||||
self._dispatch = dispatch or GLDispatch()
|
||||
self._assets: Optional[SceneAssets] = None
|
||||
self._frame: Optional[RenderFrame] = None
|
||||
self._assets: SceneAssets | None = None
|
||||
self._frame: RenderFrame | None = None
|
||||
self._settings = DisplaySettings()
|
||||
self._capabilities: Optional[GLCapabilities] = None
|
||||
self._diagnostic: Optional[Diagnostic] = None
|
||||
self._capabilities: GLCapabilities | None = None
|
||||
self._diagnostic: Diagnostic | None = None
|
||||
self._needs_scene_upload = False
|
||||
self._needs_surface_upload = False
|
||||
self._last_stats_at = 0.0
|
||||
#: What reached the screen, counted by identity, and what it cost to compose.
|
||||
self._unique_frames = 0
|
||||
self._last_frame_identity: Optional[FrameIdentity] = None
|
||||
self._last_frame_identity: FrameIdentity | None = None
|
||||
self._last_compose_ms = 0.0
|
||||
self._last_press: Optional[tuple[float, float]] = None
|
||||
self._last_press: tuple[float, float] | None = None
|
||||
self._unavailable = False
|
||||
#: Frames the display never showed, set by the coordinator. Never a
|
||||
#: recording figure.
|
||||
@@ -119,7 +120,7 @@ class Sensor3DWidget(QOpenGLWidget):
|
||||
def camera(self) -> OrbitCamera:
|
||||
return self._camera
|
||||
|
||||
def capabilities(self) -> Optional[GLCapabilities]:
|
||||
def capabilities(self) -> GLCapabilities | None:
|
||||
return self._capabilities
|
||||
|
||||
def displayed_frames(self) -> int:
|
||||
@@ -130,7 +131,7 @@ class Sensor3DWidget(QOpenGLWidget):
|
||||
"""What the frame on screen cost to compose, measured where it was composed."""
|
||||
return float(self._last_compose_ms)
|
||||
|
||||
def diagnostic(self) -> Optional[Diagnostic]:
|
||||
def diagnostic(self) -> Diagnostic | None:
|
||||
return self._diagnostic
|
||||
|
||||
@property
|
||||
|
||||
@@ -24,11 +24,12 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from PySide6.QtCore import QObject, QTimer, Signal
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtCore import QObject, QTimer, Signal
|
||||
|
||||
from orisys_studio import session_metadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -58,7 +59,7 @@ class SessionController(QObject):
|
||||
*,
|
||||
recording: Any,
|
||||
packet_for_3d: Callable[[Any], None],
|
||||
packet_sink: Optional[Callable[[Any], None]] = None,
|
||||
packet_sink: Callable[[Any], None] | None = None,
|
||||
sensor_type: str = "box",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
@@ -82,9 +83,9 @@ class SessionController(QObject):
|
||||
self._connect_slow_notified = False
|
||||
self._raw_fallback_notified = False
|
||||
# --- per-session telemetry (read by the rail, the plots and the record) -----
|
||||
self.last_sample_valid: Optional[bool] = None
|
||||
self.latest_fps: Optional[float] = None
|
||||
self.latest_process_ms: Optional[float] = None
|
||||
self.last_sample_valid: bool | None = None
|
||||
self.latest_fps: float | None = None
|
||||
self.latest_process_ms: float | None = None
|
||||
self.latest_capture_fps = 0.0
|
||||
self.latest_capture_dropped = 0
|
||||
#: How many readouts failed this session, for the status line's count.
|
||||
@@ -380,7 +381,7 @@ class SessionController(QObject):
|
||||
return consumer() if callable(consumer) else None
|
||||
|
||||
# ------------------------------------------------------------- presenter help
|
||||
def check_capture_negotiation(self) -> Optional[str]:
|
||||
def check_capture_negotiation(self) -> str | None:
|
||||
"""A once-per-session warning when the camera delivers less than configured.
|
||||
|
||||
The evidence is the *measured* capture rate, not the driver's reported one:
|
||||
@@ -429,7 +430,7 @@ class SessionController(QObject):
|
||||
"""The status line for a failed readout (the phase is deliberately untouched)."""
|
||||
return t("status.sample_invalid", count=self.record_error_count, reason=reason)
|
||||
|
||||
def take_dropped_message(self, dropped: int) -> Optional[str]:
|
||||
def take_dropped_message(self, dropped: int) -> str | None:
|
||||
"""One message per new batch of samples the memory bound discarded."""
|
||||
if int(dropped) <= self.dropped_reported or int(dropped) <= 0:
|
||||
return None
|
||||
|
||||
@@ -25,9 +25,10 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
from typing import Any
|
||||
|
||||
from orisys_app_shared.force.filters import FilterSettings, settings_from_mapping
|
||||
from orisys_app_shared.record import RecordConfig
|
||||
@@ -198,7 +199,8 @@ class SessionFacts:
|
||||
|
||||
def build_session_metadata(config: RecordConfig, facts: SessionFacts) -> dict:
|
||||
"""The ``session.json`` metadata written when a recording starts."""
|
||||
from orisys_app_shared import API_VERSION, __version__ as shared_version
|
||||
from orisys_app_shared import API_VERSION
|
||||
from orisys_app_shared import __version__ as shared_version
|
||||
|
||||
from orisys_studio import __version__ as app_version
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.paths import logo_topbar_path
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.paths import logo_topbar_path
|
||||
|
||||
from orisys_studio import __version__ as app_version
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,14 @@ where the picture comes from (live / replay), which words the footer.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
ARROW_PURE,
|
||||
ARROW_STITCHED,
|
||||
DEPTH,
|
||||
ImagePlan,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QImage, QPainter, QPixmap, QTransform
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
@@ -23,15 +28,6 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
ARROW_PURE,
|
||||
ARROW_STITCHED,
|
||||
DEPTH,
|
||||
ImagePlan,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
|
||||
from orisys_studio.ui.fit_text import ShrinkableLabel
|
||||
|
||||
VIEW_MODES = (ARROW_PURE, ARROW_STITCHED, DEPTH)
|
||||
@@ -340,7 +336,7 @@ class ArrowPanel(QFrame):
|
||||
self._placeholder = self._empty_message()
|
||||
self.image_label.setText(self._placeholder)
|
||||
|
||||
def set_plan(self, plan: Optional[ImagePlan]) -> None:
|
||||
def set_plan(self, plan: ImagePlan | None) -> None:
|
||||
"""Adopt what the frame on screen can be shown as.
|
||||
|
||||
The modes come from the frame's own fields (see ``image_backend.plan_image``),
|
||||
|
||||
@@ -14,13 +14,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor, QPainter, QPixmap
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.paths import logo_topbar_path
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor, QPainter, QPixmap
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.force.calibration_io import (
|
||||
ChannelKey,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.paths import calibration_export_dir
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor, QFont, QFontDatabase
|
||||
from PySide6.QtWidgets import (
|
||||
@@ -28,19 +34,6 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_app_shared.force.calibration_io import (
|
||||
CalSample,
|
||||
CalSessionStore,
|
||||
ChannelKey,
|
||||
meta_from_sensor_json,
|
||||
)
|
||||
from orisys_app_shared.force.calibration_session import (
|
||||
session_filename,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.paths import calibration_export_dir
|
||||
from orisys_app_shared.theme import color
|
||||
|
||||
from orisys_studio.calibration_service import CalibrationService
|
||||
from orisys_studio.ui.cal_point_dialog import CalibrationPointDialog
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ the three-panel layout, while a single point can still be examined in full.
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import Qt
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtGui import QFont, QFontDatabase
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
@@ -17,10 +18,6 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from orisys_app_shared.force import NA
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
except ImportError as exc: # pragma: no cover
|
||||
|
||||
@@ -5,16 +5,6 @@ from __future__ import annotations
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import Qt, QSize, Signal
|
||||
from PySide6.QtGui import QColor, QFont, QFontDatabase, QFontMetrics
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.force import MetricHistory
|
||||
from orisys_app_shared.force.history import DEFAULT_MAX_FPS
|
||||
@@ -27,6 +17,15 @@ from orisys_app_shared.force.source import StoreCurveSource
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.record.sample import plottable_axis_mask
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtGui import QColor, QFont, QFontDatabase, QFontMetrics
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from orisys_studio.ui import icons
|
||||
from orisys_studio.ui.icons import ICON_SIZE
|
||||
@@ -130,11 +129,11 @@ def _mono_font(point_size: int = 9, *, bold: bool = False) -> QFont:
|
||||
class _Sync:
|
||||
"""Marks a block of range changes as programmatic (not a user gesture)."""
|
||||
|
||||
def __init__(self, panel: "ForcePlotsPanel") -> None:
|
||||
def __init__(self, panel: ForcePlotsPanel) -> None:
|
||||
self._panel = panel
|
||||
self._previous = panel._syncing
|
||||
|
||||
def __enter__(self) -> "_Sync":
|
||||
def __enter__(self) -> _Sync:
|
||||
self._panel._syncing = True
|
||||
return self
|
||||
|
||||
@@ -1054,7 +1053,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self._window = (x0, x1)
|
||||
self.refresh_from_store()
|
||||
|
||||
def _guard(self) -> "_Sync":
|
||||
def _guard(self) -> _Sync:
|
||||
"""Context manager marking range changes as programmatic."""
|
||||
return _Sync(self)
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@ from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtCore import QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QIcon, QPainter, QPen, QPixmap
|
||||
|
||||
from orisys_app_shared.theme import color
|
||||
|
||||
#: Names of the icons this module can paint.
|
||||
NAMES = ("zoom_fit", "box_zoom", "latest", "settings", "reset_view")
|
||||
|
||||
|
||||
@@ -13,9 +13,15 @@ acquisition, and no path that reports a 3D problem as a recording problem.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
Diagnostic,
|
||||
ExpectedGeometry,
|
||||
)
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
@@ -27,13 +33,6 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
Diagnostic,
|
||||
ExpectedGeometry,
|
||||
SceneAssets,
|
||||
diagnostic,
|
||||
)
|
||||
from orisys_studio.scene3d.asset_source import (
|
||||
SceneAssetLoader,
|
||||
SceneSource,
|
||||
@@ -44,6 +43,7 @@ from orisys_studio.ui import icons
|
||||
|
||||
if TYPE_CHECKING: # the view imports GL bindings lazily; the type is enough here
|
||||
from orisys_app_shared.acquisition.geometry import DeviceGeometry
|
||||
|
||||
from orisys_studio.scene3d.view import Scene3DView
|
||||
|
||||
#: Shown when the pane is on but this sensor has no 3D scene at all (box, mini).
|
||||
@@ -81,17 +81,17 @@ class Mesh3dPanel(QFrame):
|
||||
enabledChanged = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self, parent: Optional[QWidget] = None, *, theme: str = "dark"
|
||||
self, parent: QWidget | None = None, *, theme: str = "dark"
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("contentPanel")
|
||||
self._theme = str(theme)
|
||||
self._view: Optional["Scene3DView"] = None
|
||||
self._loader: Optional[SceneAssetLoader] = None
|
||||
self._source: Optional[SceneSource] = None
|
||||
self._view: Scene3DView | None = None
|
||||
self._loader: SceneAssetLoader | None = None
|
||||
self._source: SceneSource | None = None
|
||||
self._sensor_type = ""
|
||||
self._expected: Optional[ExpectedGeometry] = None
|
||||
self._expected_provider: "Optional[Callable[[], DeviceGeometry]]" = None
|
||||
self._expected: ExpectedGeometry | None = None
|
||||
self._expected_provider: Callable[[], DeviceGeometry] | None = None
|
||||
self._source_kind = "live"
|
||||
self._enabled = False
|
||||
self._loaded = False
|
||||
@@ -103,8 +103,8 @@ class Mesh3dPanel(QFrame):
|
||||
self._keys = None
|
||||
#: The package whose scene is in the view, so switching sensor away and back
|
||||
#: does not reload it (and does not pretend to be loading it either).
|
||||
self._loaded_path: Optional[Path] = None
|
||||
self._reported: Optional[Diagnostic] = None
|
||||
self._loaded_path: Path | None = None
|
||||
self._reported: Diagnostic | None = None
|
||||
self._pending_settings = None
|
||||
self._message_kind = _UNAVAILABLE
|
||||
self._message_args: dict[str, str] = {"reason": ""}
|
||||
@@ -225,8 +225,8 @@ class Mesh3dPanel(QFrame):
|
||||
self,
|
||||
sensor_type: str,
|
||||
*,
|
||||
expected: Optional[ExpectedGeometry] = None,
|
||||
expected_provider: "Optional[Callable[[], DeviceGeometry]]" = None,
|
||||
expected: ExpectedGeometry | None = None,
|
||||
expected_provider: Callable[[], DeviceGeometry] | None = None,
|
||||
package_override: str | Path | None = None,
|
||||
) -> None:
|
||||
"""Tell the pane which sensor it is looking at. Cheap; no IO.
|
||||
@@ -276,7 +276,7 @@ class Mesh3dPanel(QFrame):
|
||||
return
|
||||
self._show_message(_LOADING, name=self._source.path.name)
|
||||
|
||||
def attach_view(self, view: "Scene3DView") -> None:
|
||||
def attach_view(self, view: Scene3DView) -> None:
|
||||
"""Adopt a view: the real one (built lazily) or a double in a test.
|
||||
|
||||
Public because it is the seam tests use — poking ``_view`` from a test would
|
||||
@@ -308,7 +308,7 @@ class Mesh3dPanel(QFrame):
|
||||
if not view.available:
|
||||
self._show_message(_UNAVAILABLE, reason=view.reason)
|
||||
|
||||
def ensure_view(self) -> Optional[QWidget]:
|
||||
def ensure_view(self) -> QWidget | None:
|
||||
"""Build the 3D view on first use, so a headless machine never imports GL."""
|
||||
if self._view is not None or self._sensor_type != "finger":
|
||||
return self._view
|
||||
@@ -448,11 +448,11 @@ class Mesh3dPanel(QFrame):
|
||||
|
||||
# ------------------------------------------------------------------ inputs
|
||||
@property
|
||||
def view(self) -> Optional["Scene3DView"]:
|
||||
def view(self) -> Scene3DView | None:
|
||||
return self._view
|
||||
|
||||
@property
|
||||
def last_diagnostic(self) -> Optional[Diagnostic]:
|
||||
def last_diagnostic(self) -> Diagnostic | None:
|
||||
return self._reported
|
||||
|
||||
@property
|
||||
@@ -471,7 +471,7 @@ class Mesh3dPanel(QFrame):
|
||||
"""Whether a 3D picture is possible here at all (GL and an asset)."""
|
||||
return self._view is not None and bool(self._view.available)
|
||||
|
||||
def scene_path(self) -> Optional[Path]:
|
||||
def scene_path(self) -> Path | None:
|
||||
return None if self._source is None else self._source.path
|
||||
|
||||
def set_source_kind(self, kind: str) -> None:
|
||||
|
||||
@@ -10,6 +10,12 @@ reports as done is what the record will say (see ``force/filters.py``).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.force.filters import (
|
||||
FilterSettings,
|
||||
available_presets,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
@@ -25,14 +31,6 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.force.filters import (
|
||||
SUGGESTED_LOWPASS_HZ,
|
||||
FilterSettings,
|
||||
available_presets,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
DEFAULT_PLOT_WINDOW_SEC = 4.0
|
||||
#: Lower bounds. FN is allowed below zero: a resting normal force has no reason
|
||||
#: to sit exactly on the frame, and with the lower bound at 0 the idle curve is
|
||||
|
||||
@@ -14,10 +14,9 @@ timer without touching the writer thread.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
#: The recording channel is idle, never started in this session.
|
||||
IDLE = "idle"
|
||||
|
||||
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QFrame,
|
||||
@@ -26,8 +27,6 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
from orisys_studio.ui.fit_text import ShrinkableLabel
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ own labels and tooltips instead of a bare "配置" combo.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.sensors import list_configs_for_type, list_sensor_types
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
@@ -18,9 +20,6 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.sensors import list_configs_for_type, list_sensor_types
|
||||
|
||||
CONTROL_HEIGHT = 30
|
||||
#: Both combos stay narrow: this row shares one line with the session state.
|
||||
TYPE_WIDTH = 76
|
||||
|
||||
@@ -19,6 +19,8 @@ scrolls sideways instead of squeezing the controls into an unreadable row.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
@@ -33,9 +35,6 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
|
||||
from orisys_studio.ui.fit_text import ShrinkableLabel
|
||||
from orisys_studio.ui.record_status import RecordStatusChip
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Protocol
|
||||
from typing import Any, Protocol
|
||||
|
||||
from PySide6.QtCore import QObject
|
||||
|
||||
@@ -80,7 +80,7 @@ class ViewLayoutController(QObject):
|
||||
default_splitter_sizes: tuple,
|
||||
min_panel_widths: dict,
|
||||
default_3d_pane_width: int,
|
||||
parent: Optional[QObject] = None,
|
||||
parent: QObject | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self._host = host
|
||||
@@ -103,7 +103,7 @@ class ViewLayoutController(QObject):
|
||||
self._view_preset = preset if preset in self._presets else "custom"
|
||||
self._show_cal = bool(host.prefs.get("show_cal_panel", False))
|
||||
self._show_finger_3d = bool(host.prefs.get("show_finger_3d", True))
|
||||
self._actions: Optional[LayoutActions] = None
|
||||
self._actions: LayoutActions | None = None
|
||||
|
||||
# ------------------------------------------------------------------ wiring
|
||||
def bind_actions(
|
||||
@@ -244,7 +244,7 @@ class ViewLayoutController(QObject):
|
||||
self._set_check(self._actions.settings, self._toolbar.settings_expanded())
|
||||
|
||||
@staticmethod
|
||||
def _set_check(action: Any, checked: bool, *, enabled: Optional[bool] = None) -> None:
|
||||
def _set_check(action: Any, checked: bool, *, enabled: bool | None = None) -> None:
|
||||
"""Set a checkable entry without letting it drive its own handler."""
|
||||
if action is None:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user