added ruff test

This commit is contained in:
ykcheungab
2026-09-27 19:20:43 +08:00
parent bcab2790bf
commit fa2703968e
178 changed files with 3277 additions and 1034 deletions
+23 -28
View File
@@ -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
+5 -4
View File
@@ -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__)
+8 -7
View File
@@ -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(
+3 -5
View File
@@ -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
+24 -25
View File
@@ -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")
+32 -31
View File
@@ -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
+18 -17
View File
@@ -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__)
+6 -13
View File
@@ -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
+12 -13
View File
@@ -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)
+1 -2
View File
@@ -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
+2 -3
View File
@@ -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
+4 -4
View File
@@ -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
+1 -1
View File
@@ -62,8 +62,8 @@ def qt_app():
@pytest.fixture
def make_window(qt_app, tmp_path, monkeypatch):
"""Build a ``StudioMainWindow`` whose preference writes stay in tmp_path."""
from orisys_app_shared import prefs as prefs_pkg
from orisys_app_shared import paths as paths_mod
from orisys_app_shared import prefs as prefs_pkg
from orisys_studio.app_window import StudioMainWindow
prefs_file = tmp_path / "preference.json"
@@ -9,7 +9,6 @@ the curves and the value cards were actually given.
from __future__ import annotations
import numpy as np
from orisys_app_shared.force.session import SessionSample, SessionStore
from orisys_studio.ui.force_plots import ForcePlotsPanel, _nice_ticks, format_force
-1
View File
@@ -14,7 +14,6 @@ button, is what tells stopped from paused.
from __future__ import annotations
from orisys_app_shared.i18n import t
from tests.conftest import finish_run, running_window, show_window
@@ -13,7 +13,6 @@ window-level tests): these are the *rules*, not the decoder.
from __future__ import annotations
import pytest
from orisys_app_shared.i18n import t
from orisys_studio import replay_controller
from orisys_studio.replay_controller import ReplayController
+8 -11
View File
@@ -9,16 +9,13 @@ the grid does not match, with a diagnostic that says which shapes were seen.
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
import numpy as np
import pytest
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.contracts import (
ExpectedGeometry,
FrameIdentity,
ThreeDError,
validate_surface_frame,
)
from orisys_studio.scene3d.adapters import (
@@ -40,16 +37,16 @@ class FakePacket:
behaviour under test.
"""
flow: Optional[np.ndarray] = None
depth: Optional[np.ndarray] = None
valid_mask: Optional[np.ndarray] = None
flow: np.ndarray | None = None
depth: np.ndarray | None = None
valid_mask: np.ndarray | None = None
frame_id: int = 1
invalid_reason: str = ""
flow_valid: Optional[bool] = True
flow_frame_id: Optional[int] = None
depth_valid: Optional[bool] = True
depth_frame_id: Optional[int] = None
forces_valid: Optional[bool] = True
flow_valid: bool | None = True
flow_frame_id: int | None = None
depth_valid: bool | None = True
depth_frame_id: int | None = None
forces_valid: bool | None = True
def _identity(generation: int = 1) -> FrameIdentity:
@@ -15,7 +15,6 @@ from dataclasses import replace
import numpy as np
import pytest
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.composer import SceneComposer
from orisys_app_shared.scene3d.contracts import (
+5 -2
View File
@@ -23,7 +23,6 @@ import os
import numpy as np
import pytest
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.contracts import (
ColorScale,
@@ -105,7 +104,11 @@ def _pump(qt_app, widget, predicate, *, timeout_s: float = 5.0) -> bool:
def gl_widget(qt_app):
"""A real, shown QOpenGLWidget with the synthetic scene, driven by Qt."""
from orisys_studio.scene3d.camera import OrbitCamera
from orisys_studio.scene3d.gl_backend import GLDispatch, GLRenderer, configure_surface_format
from orisys_studio.scene3d.gl_backend import (
GLDispatch,
GLRenderer,
configure_surface_format,
)
from orisys_studio.scene3d.widget import Sensor3DWidget
configure_surface_format("desktop33")
@@ -3,7 +3,6 @@
from __future__ import annotations
import numpy as np
from orisys_app_shared.acquisition.arrows import _policy_from_style
from orisys_app_shared.scene3d.composer import SceneComposer
from orisys_app_shared.scene3d.contracts import (
+7 -4
View File
@@ -13,11 +13,10 @@ view menu / the pane's header both reach that dialog.
from __future__ import annotations
import pytest
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget
from orisys_app_shared.i18n import t
from orisys_studio.ui import icons
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget
@pytest.fixture
@@ -186,7 +185,11 @@ def test_the_pane_offers_the_two_quantities_and_says_which_one_it_drew(panel):
def test_the_diagnostics_switch_governs_what_the_pane_explains(panel):
"""Warnings are claims about the measurement and always show; info is explanation."""
from orisys_app_shared.scene3d.contracts import FieldDescriptor, RenderStats, diagnostic
from orisys_app_shared.scene3d.contracts import (
FieldDescriptor,
RenderStats,
diagnostic,
)
panel.set_descriptor(FieldDescriptor("flow_magnitude", "px", "approximate", False))
panel.set_render_stats(
@@ -13,7 +13,6 @@ import json
from pathlib import Path
import pytest
from orisys_app_shared.scene3d import protect, synthetic
from orisys_app_shared.scene3d.contracts import ExpectedGeometry, ThreeDError
from orisys_app_shared.scene3d.package import write_package
@@ -404,8 +404,6 @@ def _render_region(qt_app, assets, island, uvs, preset_name, viewport):
RenderFrame,
freeze,
)
from PySide6.QtGui import QImage
from orisys_studio.scene3d import gl_backend
from orisys_studio.scene3d.camera import OrbitCamera
from orisys_studio.scene3d.gl_backend import (
@@ -415,6 +413,7 @@ def _render_region(qt_app, assets, island, uvs, preset_name, viewport):
lut_rgba,
)
from orisys_studio.scene3d.widget import Sensor3DWidget
from PySide6.QtGui import QImage
configure_surface_format("desktop33")
renderer = GLRenderer()
+2 -3
View File
@@ -49,9 +49,8 @@ def test_the_skeleton_does_not_import_a_gl_binding(qt_app):
def test_configure_surface_format_selects_a_complete_profile(qt_app, restore_surface_format):
from PySide6.QtGui import QSurfaceFormat
from orisys_studio.scene3d.gl_backend import configure_surface_format
from PySide6.QtGui import QSurfaceFormat
configure_surface_format("desktop33")
desktop = QSurfaceFormat.defaultFormat()
@@ -72,7 +71,7 @@ def test_configure_surface_format_selects_a_complete_profile(qt_app, restore_sur
def test_the_widget_holds_cpu_state_and_touches_no_context(qt_app):
"""Constructing the widget must not need a current context or a GPU."""
from orisys_app_shared.scene3d.contracts import DisplaySettings, FrameIdentity
from orisys_app_shared.scene3d.contracts import DisplaySettings
from orisys_studio.scene3d.camera import OrbitCamera
from orisys_studio.scene3d.gl_backend import GLRenderer
from orisys_studio.scene3d.widget import Sensor3DWidget
-1
View File
@@ -9,7 +9,6 @@ from __future__ import annotations
import numpy as np
import pytest
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.contracts import (
CameraPreset,
+13 -7
View File
@@ -20,6 +20,7 @@ mathematics (the camera, the sampler and the composer have their own files):
from __future__ import annotations
import math
import time
from concurrent.futures import Future
from dataclasses import replace
@@ -27,17 +28,14 @@ from pathlib import Path
import numpy as np
import pytest
import ui_check
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget
from orisys_app_shared.i18n import t
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.contracts import (
UNAVAILABLE_DESCRIPTOR,
ColorScale,
DisplaySettings,
FieldDescriptor,
UNAVAILABLE_DESCRIPTOR,
)
from orisys_app_shared.scene3d.package import write_package
from orisys_studio.app_window import MIN_PANEL_WIDTHS
@@ -49,9 +47,15 @@ from orisys_studio.scene3d.asset_source import (
load_package,
no_asset_diagnostic,
)
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget
from tests.conftest import (
finish_run as _finish,
)
from tests.conftest import (
running_window as _running_window,
)
from tests.conftest import (
show_window as _show,
)
@@ -60,7 +64,7 @@ from tests.conftest import (
def write_poc(tmp_path):
"""A real package on disk, produced by the shipping writer."""
def _write(name: str = "finger-display-1.0.0") -> "object":
def _write(name: str = "finger-display-1.0.0") -> object:
path = tmp_path / f"{name}{PACKAGE_SUFFIX}"
write_package(
synthetic.poc_scene(),
@@ -520,7 +524,9 @@ def test_a_hand_edited_settings_block_is_repaired(block):
assert settings.field in scene3d_prefs.FIELD_CHOICES
if settings.color_scale is not None:
lower, upper = settings.color_scale.lower, settings.color_scale.upper
assert lower == lower and upper == upper, "a NaN survived the repair"
# Not a tautology: ``float("nan")`` is in the parametrisation above, and this
# is the assertion that the repair actually replaced it.
assert not math.isnan(lower) and not math.isnan(upper), "a NaN survived the repair"
assert upper > lower
assert abs(lower) <= scene3d_prefs.MAX_SCALE_BOUND
assert abs(upper) <= scene3d_prefs.MAX_SCALE_BOUND
@@ -1129,8 +1135,8 @@ def test_every_run_gets_a_new_generation(make_window, qt_app):
Without this the pane accepts the first run's frames and refuses every later run's,
so the picture stays on the first run's last frame forever.
"""
from orisys_app_shared.scene3d.contracts import FrameIdentity
import ui_check
from orisys_app_shared.scene3d.contracts import FrameIdentity
window = _show(make_window(sensor_type="finger"), (1440, 880))
try:
-2
View File
@@ -10,8 +10,6 @@ from __future__ import annotations
from pathlib import Path
import pytest
from orisys_app_shared.sensors import list_configs_for_type, list_sensor_types
from orisys_studio import app_window
@@ -9,7 +9,6 @@ recording coordinator and fake workers.
from __future__ import annotations
import pytest
from orisys_studio import session_controller
from orisys_studio.session_controller import SessionController
+1 -2
View File
@@ -14,7 +14,6 @@ from types import SimpleNamespace
import numpy as np
import pytest
from orisys_app_shared.force.filters import FilterSettings
from orisys_app_shared.record import RecordConfig
from orisys_studio import session_metadata as sm
@@ -133,7 +132,7 @@ def test_the_session_block_states_the_device_the_filters_and_the_picture():
)
worker = SimpleNamespace(
capture_status=lambda: {"requested_fps": 30.0, "measured_fps": 21.0},
filter_state=lambda: {},
filter_state=dict,
filter_settings=lambda: FilterSettings(),
filters_unsupported=lambda: (),
)
@@ -13,7 +13,6 @@ Written against the requirement rather than the implementation:
from __future__ import annotations
import ui_check
from tests.conftest import finish_run, running_window, show_window
+26 -40
View File
@@ -5,14 +5,18 @@ from __future__ import annotations
from pathlib import Path
import numpy as np
import pytest
import ui_check
from tests.conftest import (
finish_run as _finish,
)
from tests.conftest import (
running_window as _running_window,
)
from tests.conftest import (
show_window as _show,
)
from tests.conftest import (
visible_descendants,
)
@@ -369,7 +373,6 @@ def test_the_panel_shows_the_numbers_of_the_file_it_exports(make_window, record_
from pathlib import Path
import orisys.calibration as sdk_calibration
from orisys_app_shared.force.calibration_io import build_force_calibration_v2
window = _show(make_window(), (1400, 880))
@@ -529,8 +532,11 @@ def test_applying_another_devices_calibration_is_flagged(make_window, record_roo
"""The apply path states a sensor/config mismatch instead of silently applying."""
import json
from orisys_app_shared.force.calibration_io import build_export_model, write_force_calibration
from orisys_app_shared.force.calibration_io import CalSample, CalSessionStore
from orisys_app_shared.force.calibration_io import (
CalSample,
CalSessionStore,
build_export_model,
)
window, stuck = _running_window(make_window)
store = CalSessionStore()
@@ -816,7 +822,6 @@ def test_a_confirmed_calibration_is_snapshotted_into_the_recording(make_window,
from pathlib import Path
import orisys_studio.app_window as app_module
from orisys_app_shared.i18n import t
window, stuck = _running_window(make_window)
app_module.recordings_dir = lambda: record_root
@@ -1179,9 +1184,8 @@ def test_the_live_rail_is_a_play_pause_toggle_with_its_own_stop(make_window):
icon-only pause button next to it, so the same position meant two different
things depending on which view was on screen.
"""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
stuck = ui_check._StuckWorker()
@@ -1232,7 +1236,6 @@ def test_the_live_rail_is_a_play_pause_toggle_with_its_own_stop(make_window):
def test_record_strip_separates_acquiring_from_recording(make_window):
from orisys_app_shared.i18n import t
from orisys_app_shared.record.experiment import RecordStats
from orisys_studio.ui import record_status
window, stuck = _running_window(make_window)
@@ -1315,9 +1318,8 @@ def test_overview_region_is_the_timeline_control(make_window):
def test_the_charts_start_in_follow_mode(make_window):
"""Every live entry point resumes following; only browsing is a gesture."""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window, stuck = _running_window(make_window)
panel = window.force_plots
@@ -1424,9 +1426,8 @@ def test_the_arrow_legend_follows_the_view_mode(make_window):
def test_the_sensor_summary_opens_the_dialog(make_window):
"""The rail states the sensor; the pickers live in the dialog it opens."""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
assert window.toolbar.btn_sensor.text() == t(
@@ -1614,9 +1615,8 @@ def test_the_replay_view_is_the_live_view_with_a_video_source(make_window):
def test_the_replay_view_loads_a_recording_and_scrubs_it(make_window, replay_recording):
"""The replay view reads the recording: charts, 2D panel and transport."""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
@@ -1726,7 +1726,6 @@ def test_the_unit_chip_switches_the_charts_and_the_toolbar(make_window):
"""One click on the unit chip changes the unit everywhere, not just locally."""
from orisys_app_shared.i18n import t
from orisys_studio.ui.force_plots import unit_display
from PySide6.QtWidgets import QPushButton
window = _show(make_window(), (1440, 880))
@@ -1813,7 +1812,6 @@ def test_the_video_follows_the_sensor_rate_and_keeps_every_row(make_window, reco
import csv
import cv2
import orisys_studio.app_window as app_module
window, stuck = _running_window(make_window)
@@ -1911,9 +1909,8 @@ def test_the_readout_describes_the_point_the_cursor_is_on(
A replay used to state the live acquisition in cards sitting next to a chart
of 7.5e5 raw counts, which reads as "the replay never opened".
"""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
@@ -1945,9 +1942,8 @@ def test_a_raw_only_recording_is_never_shown_in_newton(make_window, replay_recor
for the axes it calibrated. Asking for newton anyway drew three blank charts
and a dash in every card, which looks exactly like "the replay did not load".
"""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
window.toolbar.set_units("newton")
@@ -2056,9 +2052,8 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
there: plain playback and a recompute pass. The tracker reset owns no device here,
so it is disabled rather than repurposed.
"""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
@@ -2135,9 +2130,8 @@ def test_escape_stops_the_replay_too(make_window, replay_recording):
def test_the_live_view_keeps_the_tracker_reset(make_window):
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_live.click()
@@ -2160,9 +2154,8 @@ def test_a_bare_video_plays_even_though_it_is_not_a_recording(make_window, recor
import cv2
import numpy as np
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
bare = Path(record_root) / "bare_video.mp4"
writer = cv2.VideoWriter(
@@ -2223,15 +2216,14 @@ def test_the_replay_and_the_live_paths_share_one_image_backend(make_window, reco
"""
import cv2
import numpy as np
from PySide6.QtWidgets import QApplication
import orisys_studio.app_window as app_module
from orisys_app_shared.acquisition.image_backend import (
DERIVED_MODES,
ImageFrame,
SOURCE_LIVE,
SOURCE_RECOMPUTE,
ImageFrame,
)
from PySide6.QtWidgets import QApplication
bare = Path(record_root) / "backend_video.mp4"
writer = cv2.VideoWriter(str(bare), cv2.VideoWriter_fourcc(*"mp4v"), 30.0, (64, 48))
@@ -2318,9 +2310,8 @@ def test_the_replay_entries_exist_only_in_the_replay_view(make_window, record_ro
"""
import cv2
import numpy as np
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
bare = Path(record_root) / "gated_video.mp4"
writer = cv2.VideoWriter(str(bare), cv2.VideoWriter_fourcc(*"mp4v"), 30.0, (64, 48))
@@ -2373,8 +2364,6 @@ def test_a_file_that_is_not_a_video_at_all_is_refused_with_a_reason(
"""Not a recording *and* not decodable: say what is missing and offer recompute."""
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
broken = Path(record_root) / "not_really_a_video.mp4"
broken.write_text("this is not a video at all\n", encoding="utf-8")
@@ -2404,9 +2393,8 @@ def test_a_file_that_is_not_a_video_at_all_is_refused_with_a_reason(
def test_a_video_inside_a_recording_opens_the_recording_and_plays(
make_window, replay_recording
):
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_playback.click()
@@ -2432,9 +2420,8 @@ def test_a_recording_without_a_video_says_it_cannot_be_played(make_window, recor
import csv
import json
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
directory = Path(record_root) / "no_video_recording"
directory.mkdir(parents=True, exist_ok=True)
@@ -2531,9 +2518,8 @@ def test_the_status_bar_is_a_notice_line_not_a_state_line(make_window):
"""
import time
from PySide6.QtWidgets import QApplication
import orisys_studio.app_window as app_module
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
assert not hasattr(window, "device_label"), (
+2 -3
View File
@@ -8,8 +8,6 @@ both can be stood in for — which is the point of the split, and what these tes
from __future__ import annotations
import pytest
from orisys_studio.view_layout import ViewLayoutController
VIEW_PRESETS = ("custom", "live", "replay", "calibration")
@@ -295,7 +293,8 @@ def test_a_preset_that_gives_no_width_to_the_3d_pane_takes_it_from_the_widest():
def test_the_shipped_live_layout_makes_the_3d_pane_the_widest_panel():
"""The 3D view is the subject when it is on, not an inset beside the charts."""
from orisys_studio.app_window import DEFAULT_SPLITTER_SIZES, VIEW_SPLITTER_SIZES as LIVE
from orisys_studio.app_window import DEFAULT_SPLITTER_SIZES
from orisys_studio.app_window import VIEW_SPLITTER_SIZES as LIVE
mesh, image, charts = DEFAULT_SPLITTER_SIZES
assert mesh > image, DEFAULT_SPLITTER_SIZES
+16 -12
View File
@@ -18,6 +18,7 @@ import sys
import threading
import time
from pathlib import Path
APP_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = APP_ROOT.parents[1]
for _path in (APP_ROOT / "src", REPO_ROOT / "shared" / "src"):
@@ -185,7 +186,6 @@ def _fake_packet(
something visible (see ``ui_snapshot --scene replay``).
"""
import numpy as np
from orisys_app_shared.acquisition.sdk_worker import SdkPacket
nan = float("nan")
@@ -558,12 +558,10 @@ def audit_palette(verbose: bool = False) -> list[str]:
def main() -> int:
from PySide6.QtWidgets import QApplication
from orisys_app_shared.force.calibration_io import CalSample
from orisys_app_shared.i18n import set_locale, t
from orisys_app_shared.theme import build_qss, load_colors
from orisys_studio.app_window import VIEW_SPLITTER_SIZES, StudioMainWindow
from orisys_studio.app_window import StudioMainWindow
from PySide6.QtWidgets import QApplication
app = QApplication.instance() or QApplication(sys.argv)
set_locale("zh_CN")
@@ -768,12 +766,14 @@ def main() -> int:
window.cal_panel.table.item(1, 5).text() == t("cal.state_excluded"),
"the excluded row does not show its state",
)
import orisys.calibration as _sdk_calibration
from orisys_app_shared.force.calibration_io import (
build_export_model,
build_force_calibration_v2 as _build_v2,
predict_channel,
)
import orisys.calibration as _sdk_calibration
from orisys_app_shared.force.calibration_io import (
build_force_calibration_v2 as _build_v2,
)
store = window.cal_panel._store
model = build_export_model(
@@ -1798,6 +1798,7 @@ def main() -> int:
section("acquisition correctness")
import csv as _csv
import shutil as _shutil
import numpy as np
# A device that only confirms its calibration a moment after the first
@@ -2090,6 +2091,10 @@ def main() -> int:
env=env,
capture_output=True,
timeout=60,
# Deliberately not ``check=True``: a non-zero exit *is* one of the things
# being measured here (a crash on shutdown), and it is reported through
# this checker's own ``check()`` below rather than as a traceback.
check=False,
)
elapsed = time.perf_counter() - started
print(f" window closed and the process returned in {elapsed:.1f}s")
@@ -2326,17 +2331,18 @@ def main() -> int:
import json as _cal_json
from unittest.mock import patch as _patch_mismatch
import orisys_studio.ui.cal_panel as _cal_panel_module
import orisys_studio.calibration_service as _cal_service_module
from PySide6.QtWidgets import QMessageBox as _QMessageBox
import orisys_studio.ui.cal_panel as _cal_panel_module
from orisys_app_shared.force.calibration_io import (
build_force_calibration_v2 as _build_v2b,
)
from orisys_app_shared.force.calibration_io import (
write_force_calibration as _write_cal,
)
from orisys_app_shared.force.calibration_session import (
build_session_document as _build_session_doc,
)
from PySide6.QtWidgets import QMessageBox as _QMessageBox
record_dir = APP_ROOT / "outputs" / "_cal_record_check"
if record_dir.exists():
@@ -3065,7 +3071,6 @@ def main() -> int:
recorder.on_packet(_fake_packet(index))
recorder.stop()
from orisys_studio.replay import ReplayEngine
# The replay lives in the main window: one engine, one progress bar, and the
# charts read the recording itself.
@@ -3314,7 +3319,6 @@ def main() -> int:
# Long-history browsing: an interval read is bounded by the interval, and the
# whole-session envelope is a band rather than every row.
from orisys_app_shared.force.source import RecordingCurveSource
from orisys_app_shared.record.index import RecordingIndex
from orisys_app_shared.record.playback import RecordingReader as _RecordingReader
reader = _RecordingReader(recording, max_rows=10)
+1 -8
View File
@@ -38,9 +38,7 @@ os.chdir(APP_ROOT)
def _replay_data(window, app) -> Path:
"""Synthesize one short recording (with visible frames) and open it."""
import numpy as np
from orisys_app_shared.record import ExperimentRecorder, RecordConfig
from ui_check import _fake_packet
root = APP_ROOT / "outputs" / "_snapshot_replay"
@@ -121,7 +119,6 @@ def _run_scene(window, app, scene: str):
window._run_timer.stop()
from orisys_app_shared.record.experiment import RecordStats
from orisys_studio.ui import record_status
directory = "20240101T101500Z_session0001_a1b2c3d4e5f6"
@@ -160,7 +157,6 @@ def _flow_scene(window, app, *, frames: int = 240) -> None:
import numpy as np
from orisys_app_shared.acquisition.sdk_worker import SdkPacket
from ui_check import _StuckWorker
stuck = _StuckWorker()
@@ -304,14 +300,12 @@ def _demo_data(window) -> None:
# Synthetic outward flow so the branded canvas and flow ramp are reviewable.
import numpy as np
from orisys_app_shared.acquisition.arrows import (
draw_arrow_display,
flow_colormap,
hex_to_bgr,
)
from orisys_app_shared.theme import color
from orisys_studio.app_window import _bgr_to_qimage
size = 320
@@ -393,11 +387,10 @@ def main() -> int:
panels = {part.strip().lower() for part in args.panels.split(",") if part.strip()}
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import set_locale
from orisys_app_shared.theme import build_qss
from orisys_studio.app_window import StudioMainWindow
from PySide6.QtWidgets import QApplication
app = QApplication(sys.argv)
set_locale(args.lang)