界面:三维面板只留画面与图例,全部配置进「三维显示设置」对话框;三维占比提为最宽

- What(隐藏配置): `ThreeDPanel` 把**所有设置**收进一个块
  (`controls_widget`:总开关、观察方向、显示外壳、放大形变+增益、色标模式与范围、显示帧率、
  复位按钮、显示统计),面板本身只保留读图需要的东西:色条 + 一行紧凑图例(数量/自动色标范围/
  来源)+ 状态行。新增 `scene3d/settings_dialog.py` 的 `Scene3dSettingsDialog`:非模态
  (调参时画面还在动),承载的就是面板那套控件本身(同一批 widget,信号通路不变),
  关闭只是隐藏(控件必须活着,下一次读设置还要用)。
  两个入口:视图菜单新增 `三维显示设置…`(`act_scene3d_settings` → `Mesh3dPanel.open_settings()`)
  与面板标题栏的齿轮;无三维可配时(box 传感器 / 无 GL / 无场景包)菜单项会把面板上的原因
  原样提示出来,而不是弹一个空对话框。标题栏另有「复位视角」图标(新画的 `reset_view` 图标,
  环形箭头,随主题着色)。
- What(提高占比): live/replay 预设把三维面板设为最宽:`VIEW_SPLITTER_SIZES` (620, 300, 520)、
  `DEFAULT_SPLITTER_SIZES` (620, 320, 500)、`DEFAULT_3D_PANE_WIDTH` 340 → 620、
  `MIN_PANEL_WIDTHS["mesh3d"]` 260 → 200(面板里没有表单要放了,画面才是重点)、
  `ViewLayoutController.apply_panel_visibility` 的拉伸权重 2 → 5(2D 3、曲线 4)。
  1440×880 实测:三维约 430 px(原约 280)、2D 约 265、曲线约 320。
- What(拍视频/评审用): `tools/ui_snapshot.py` 新增 `--scene flow3d`:驱动一批**带真实光流场**的
  packets(适配器只接受声明了 provenance 的字段),所以三维面板真的有颜色、曲线也真的在动;
  这是本次界面评审与演示视频的取景脚本。配 `--sensor finger --panels 3d --preset live`。
- What(测试): 新增 `apps/studio/tests/test_scene3d_pane_ui.py`(5 项):面板只有图例没有设置、
  对话框承载的是面板自己的控件且设置信号照旧、关闭只隐藏不销毁、标题栏两个动作可用、
  无场景时 `open_settings()` 返回 False、`reset_view` 转发、`reset_view` 图标两种主题都能画、
  视图菜单项能打开对话框(无 GL 时给原因)。`test_view_layout.py` 增一条"出厂 live 布局里
  三维最宽"的断言(并更正我自己那条 340 的旧期望)。
- 文档: `studio-3d-stages.md` 的「尚未完成」第 1 条(阶段 8 第二批)改写为"部分已做",
  写清已完成的部分与仍留后的四项(页签容器/窄窗口回退/曲线量程开关/独立诊断抽屉)。
- Why: 三维面板原来下半屏全是表单,画面被挤成一小块——拍视频要的是"三维是主角"。
  配置不是不需要,而是不该和画面抢地方;放进对话框后画面变大、参数反而更好找(一处看全)。
- Verification: pytest shared/tests apps/studio/tests(offscreen)650 passed / **2 failed** / 15 skipped,
  两个失败都是 `shared/tests/test_worker_frame_rate.py` 的**墙钟计时**用例(30 fps 设备被测成
  24–25 fps):已在 `d539ce9` 的干净 worktree 上复现同样的失败,属本机负载(DSH/Cursor/飞书 占 CPU)
  而非本次改动;真 GL 用例 `test_scene3d_gl_smoke` + `test_scene3d_view` 61 项 0 失败(1 跳过);
  `tools/ui_check.py` 587 项检查 0 失败;`.repro/locale_check.py` 0 缺失(新增 6 个 key 双语齐备)。
  目视验收:`--scene flow3d` 渲染的 1440×880 截图(拆分前后各一张)见提交说明附言。
- Not in this change: 不改三维渲染/契约/记录;不做页签容器与窄窗口回退;
  曲线量程仍是出厂 ±20 N(演示取景脚本会临时调窄,产品默认值另议)。
This commit is contained in:
ykcheungab
2026-09-15 20:55:33 +08:00
parent d539ce93ad
commit 650578d738
13 changed files with 629 additions and 53 deletions
+29 -10
View File
@@ -80,13 +80,13 @@ logger = logging.getLogger(__name__)
RUNNABLE_TYPES = frozenset({"box", "mini", "finger"})
ARROW_ROTATE_TYPES = frozenset({"box", "mini"})
DEFAULT_SPLITTER_SIZES = (340, 480, 400)
DEFAULT_SPLITTER_SIZES = (620, 320, 500)
MIN_PANEL_WIDTHS = {
# The 3D pane is the only one whose content is a real control panel rather than a
# label or a picture, and its minimum is measured under the application
# stylesheet in both languages: the widest case is English at 241 px
# (``tests/test_scene3d_view.py`` pins it), so 240 would have been 1 px short.
"mesh3d": 260,
# The 3D pane used to be the one panel whose content was a stack of form fields, so
# its minimum was measured under the stylesheet in both languages (241 px in English)
# and could not go below 260. The controls now live in the settings dialog and what
# is left wraps, so the pane can be dragged narrow again — the picture is the point.
"mesh3d": 200,
"arrow": 300,
"plots": 340,
"cal": 300,
@@ -99,9 +99,11 @@ VIEW_PRESETS = ("custom", "live", "replay", "calibration")
#: ``custom`` keeps whatever the user arranged (see ``_restore_splitter_sizes``).
#: Replay is the live layout on purpose: the two views differ in their source
#: (a camera, or a recorded video), not in their furniture.
#: The 3D pane is the widest of the three: it is the view's subject when it is on, and
#: the 2D panel is a reading of the same measurement rather than a second subject.
VIEW_SPLITTER_SIZES = {
"live": (0, 360, 1080, 0),
"replay": (0, 360, 1080, 0),
"live": (620, 300, 520, 0),
"replay": (620, 300, 520, 0),
"calibration": (0, 0, 560, 880),
}
#: The 3D pane belongs to the live view and to history browsing, and only for a
@@ -114,7 +116,7 @@ VIEW_SHOWS_3D = ("custom", "live", "replay")
#: Width the 3D pane takes from the widest other panel when a preset's table gives it
#: nothing. The table describes the two-dimensional layout; the pane is optional, and
#: "not in the table" must not mean "standing at its minimum width".
DEFAULT_3D_PANE_WIDTH = 340
DEFAULT_3D_PANE_WIDTH = 620
#: Kept under its historical name for ``tools/ui_snapshot.py``, which imports it from
@@ -239,7 +241,7 @@ class StudioMainWindow(QMainWindow):
self.status_chip = self.toolbar.status_chip
self.content_splitter = QSplitter(Qt.Horizontal)
self.mesh3d_panel = Mesh3dPanel()
self.mesh3d_panel = Mesh3dPanel(theme=self._theme)
self.arrow_panel = ArrowPanel(theme=self._theme)
self.force_plots = ForcePlotsPanel(theme=self._theme)
# The charts browse the whole session, not just their rolling buffer.
@@ -899,6 +901,13 @@ class StudioMainWindow(QMainWindow):
self.act_finger_3d.setChecked(self._show_finger_3d)
self.view_menu.addAction(self.act_finger_3d)
# Everything the 3D pane can be told lives in one dialog: the pane itself is for
# looking at (see ``Mesh3dPanel``). This is the menu way in; the pane's header
# has the gear, which is the same call.
self.act_scene3d_settings = QAction(self)
self.act_scene3d_settings.triggered.connect(self._on_scene3d_settings)
self.view_menu.addAction(self.act_scene3d_settings)
self.act_show_fps = QAction(self)
self.act_show_fps.setCheckable(True)
self.act_show_fps.setChecked(self._show_fps)
@@ -1145,6 +1154,7 @@ class StudioMainWindow(QMainWindow):
self.view_menu.setTitle(t("menu.view"))
self.act_cal_panel.setText(t("menu.calibration_panel"))
self.act_finger_3d.setText(t("menu.finger_3d"))
self.act_scene3d_settings.setText(t("menu.scene3d_settings"))
self.act_show_fps.setText(t("menu.show_fps"))
self.act_settings.setText(t("menu.settings"))
self.act_plot_settings.setText(t("menu.plot_settings"))
@@ -1350,6 +1360,7 @@ class StudioMainWindow(QMainWindow):
self.brand_bar.set_theme(theme)
self.toolbar.set_theme(theme)
self.arrow_panel.set_theme(theme)
self.mesh3d_panel.set_theme(theme)
self._refresh_status_chip()
self._refresh_status_rail()
self._refresh_record_state()
@@ -1652,6 +1663,14 @@ class StudioMainWindow(QMainWindow):
return
view.submit_packet(packet)
def _on_scene3d_settings(self) -> None:
"""Open the 3D settings dialog — or say why there is nothing to configure."""
if not self.mesh3d_panel.open_settings():
self._notify(
self.mesh3d_panel.message.text().strip()
or t("panels.mesh3d_no_scene")
)
def _on_sensor_type_changed(self, sensor_type: str) -> None:
self.prefs["sensor_type"] = str(sensor_type)
self.prefs["config_name"] = self.sensor_picker.config_name()
+45 -26
View File
@@ -136,21 +136,34 @@ class ThreeDPanel(QFrame):
self._effective_scale: Optional[RenderStats] = None
root = QVBoxLayout(self)
root.setContentsMargins(8, 6, 8, 8)
root.setContentsMargins(8, 6, 8, 6)
root.setSpacing(6)
# ------------------------------------------------------------------ controls
# Every *setting* lives in one block that the pane itself never shows. What the
# pane is for is the picture, and a column of form fields beside it was what left
# the 3D view a corner of its own splitter slot. The block is handed to
# ``Scene3dSettingsDialog`` — opened from 视图 ▸ 三维显示设置 or from the gear on
# the pane's header — and the widget attributes below stay on this object, so the
# rest of the application (and its tests) reach the controls the same way as
# before.
self.controls_widget = QWidget(self)
self.controls_widget.setObjectName("scene3dControls")
self.controls_widget.setVisible(False)
controls = QVBoxLayout(self.controls_widget)
controls.setContentsMargins(0, 0, 0, 0)
controls.setSpacing(6)
self.enable_check = QCheckBox()
self.enable_check.toggled.connect(self._on_enabled_toggled)
root.addWidget(self.enable_check)
controls.addWidget(self.enable_check)
form = QFormLayout()
form.setContentsMargins(0, 0, 0, 0)
self.preset_label = QLabel()
self.preset_combo = QComboBox()
# The pane is 240 px wide at its narrowest (``MIN_PANEL_WIDTHS["mesh3d"]``),
# so the fields are compact and the panel's own minimum stays below that.
# Without this the layout's minimum is set by two spin boxes side by side,
# the splitter cannot honour the panel width, and the picture is squeezed out.
# The dialog is a dialog: the fields are comfortable there, and the pane no
# longer has to be at least as wide as two spin boxes side by side.
self.preset_combo.setMaximumWidth(FIELD_WIDTH)
self.preset_combo.currentIndexChanged.connect(self._on_preset_chosen)
form.addRow(self.preset_label, self.preset_combo)
@@ -192,9 +205,7 @@ class ThreeDPanel(QFrame):
form.addRow(self.scale_label, self.scale_combo)
# The two bounds get their own row rather than sharing the mode's: three
# controls side by side made this row the widest thing in the pane, and a pane
# whose content demands more than ``MIN_PANEL_WIDTHS["mesh3d"]`` clips its own
# controls when the operator drags the splitter to the declared minimum.
# controls side by side made this row the widest thing in the pane.
self.range_label = QLabel()
self.lower_spin = QDoubleSpinBox()
self.lower_spin.setRange(-1e6, 1e6)
@@ -221,26 +232,39 @@ class ThreeDPanel(QFrame):
self.fps_combo.setMaximumWidth(FIELD_WIDTH)
self.fps_combo.currentIndexChanged.connect(self._emit_settings)
form.addRow(self.fps_label, self.fps_combo)
root.addLayout(form)
controls.addLayout(form)
buttons = QHBoxLayout()
self.reset_button = QPushButton()
self.reset_button.clicked.connect(self.resetRequested.emit)
buttons.addWidget(self.reset_button)
buttons.addStretch(1)
root.addLayout(buttons)
controls.addLayout(buttons)
# Display-side statistics: an engineering readout, not something to read while
# measuring, so it belongs with the settings rather than under the picture.
self.stats_label = QLabel()
self.stats_label.setObjectName("panelMeta")
self.stats_label.setWordWrap(True)
self.stats_label.setMinimumWidth(1)
controls.addWidget(self.stats_label)
controls.addStretch(1)
# -------------------------------------------------------------------- legend
# What is on screen, in three short pieces: the ramp the shader used, the numbers
# it used it for, and which source produced the field. Reading the picture needs
# the first two, so they stay; nothing here is a setting.
self.scale_bar = ColorScaleBar()
root.addWidget(self.scale_bar)
legend_row = QHBoxLayout()
legend_row.setContentsMargins(0, 0, 0, 0)
legend_row.setSpacing(8)
self.legend_label = QLabel()
self.legend_label.setObjectName("panelMeta")
self.legend_label.setWordWrap(True)
self.legend_label.setMinimumWidth(1)
root.addWidget(self.legend_label)
# The ramp the shader used, under the numbers it used it for. Text alone could
# not express an *automatic* range, and the disabled spin boxes next to it held
# values that were not being drawn with.
self.scale_bar = ColorScaleBar()
root.addWidget(self.scale_bar)
legend_row.addWidget(self.legend_label, 3)
#: Which *source* the field on screen came from. A separate line from the
#: scene state, because they answer different questions: the state says whether
@@ -250,7 +274,9 @@ class ThreeDPanel(QFrame):
self.source_label.setObjectName("panelMeta")
self.source_label.setWordWrap(True)
self.source_label.setMinimumWidth(1)
root.addWidget(self.source_label)
self.source_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
legend_row.addWidget(self.source_label, 2)
root.addLayout(legend_row)
self.state_label = QLabel()
self.state_label.setObjectName("hintText")
@@ -258,13 +284,6 @@ class ThreeDPanel(QFrame):
self.state_label.setMinimumWidth(1)
root.addWidget(self.state_label)
self.stats_label = QLabel()
self.stats_label.setObjectName("panelMeta")
self.stats_label.setWordWrap(True)
self.stats_label.setMinimumWidth(1)
root.addWidget(self.stats_label)
root.addStretch(1)
self.retranslate()
self.set_scene(None)
self.set_state("disabled")
@@ -0,0 +1,85 @@
"""``Scene3dSettingsDialog``: the 3D pane's settings, out of the picture's way.
The pane is for looking at a picture. Every *setting* the three-dimensional view has —
whether it is on, which camera preset, the housing, the exaggeration, the colour range
and the display rate — therefore lives in one dialog, opened from 视图 ▸ 三维显示设置 or
from the gear on the pane's header. What stays under the picture is only what *reading*
it needs: the colour ramp, the numbers it was drawn with, and which source produced the
field (see ``ThreeDPanel``).
The dialog owns no state of its own: it hosts the panel's control block
(``ThreeDPanel.controls_widget``) and closes with it untouched. That keeps one set of
widgets — and one set of signals — whether or not the dialog has ever been opened.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog,
QDialogButtonBox,
QLabel,
QVBoxLayout,
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
class Scene3dSettingsDialog(QDialog):
"""A modeless home for the 3D pane's controls."""
def __init__(
self, panel: "ThreeDPanel", parent: Optional[QWidget] = None
) -> None:
super().__init__(parent)
self.setObjectName("scene3dSettings")
self.setWindowTitle(t("scene3d.settings_title"))
# Modeless on purpose: the operator tunes the colour range while watching the
# picture change, which a modal dialog over the pane would prevent.
self.setModal(False)
self._panel = panel
root = QVBoxLayout(self)
root.setContentsMargins(16, 14, 16, 12)
root.setSpacing(10)
self.hint = QLabel()
self.hint.setObjectName("hintText")
self.hint.setWordWrap(True)
root.addWidget(self.hint)
# Adding the block to this layout reparents it: the controls now live here, and
# the pane keeps only its legend. They are the *same* widgets the panel holds
# references to, so nothing about the settings path changes with the dialog.
root.addWidget(panel.controls_widget)
panel.controls_widget.setVisible(True)
self.buttons = QDialogButtonBox(QDialogButtonBox.Close)
self.buttons.rejected.connect(self.close)
self.buttons.accepted.connect(self.close)
root.addWidget(self.buttons)
self.retranslate()
def retranslate(self) -> None:
self.setWindowTitle(t("scene3d.settings_title"))
self.hint.setText(t("scene3d.settings_hint"))
# ------------------------------------------------------------------ Qt events
def closeEvent(self, event) -> None: # noqa: N802 - Qt naming
"""Closing *hides* the dialog: the controls have to stay alive in it."""
self.hide()
event.ignore()
def keyPressEvent(self, event) -> None: # noqa: N802 - Qt naming
"""Esc closes the dialog, as it does everywhere else in this application."""
if event.key() == Qt.Key_Escape:
self.hide()
return
super().keyPressEvent(event)
@@ -117,6 +117,9 @@ class Scene3DView(QWidget):
self._device_verified = False
#: Frames the producer did not even build (a frame was already waiting).
self._skipped_snapshots = 0
#: The settings dialog, built on first use. Held (never deleted) because the
#: control widgets live in it once it has been opened.
self._settings_dialog = None
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(0, 0, 0, 0)
@@ -425,6 +428,29 @@ class Scene3DView(QWidget):
if self._panel is not None:
self._panel.set_capabilities(self.capabilities())
# ------------------------------------------------------------------ settings
def open_settings(self) -> bool:
"""Show the settings dialog. ``False`` when there is nothing to configure.
The dialog hosts the panel's control block, so it is built here (where the panel
is) and only once: a second open reuses it, and the controls' signals were wired
at construction either way.
"""
if self._panel is None:
return False
if self._settings_dialog is None:
from orisys_studio.scene3d.settings_dialog import Scene3dSettingsDialog
self._settings_dialog = Scene3dSettingsDialog(self._panel, self)
self._settings_dialog.show()
self._settings_dialog.raise_()
self._settings_dialog.activateWindow()
return True
def settings_dialog(self):
"""The dialog, if it has been opened (for tests and for the window)."""
return self._settings_dialog
def begin_session(self) -> None:
"""The application is starting an acquisition session for this pane.
@@ -636,6 +662,10 @@ class Scene3DView(QWidget):
def close(self) -> None:
"""Release the GPU resources, stop the worker. Idempotent."""
if self._settings_dialog is not None:
# Hidden, not deleted: the control widgets live in it, and the panel keeps
# referring to them (a session's display settings are read back from there).
self._settings_dialog.hide()
if self._coordinator is not None:
self._coordinator.close()
if self._widget is not None:
+39 -1
View File
@@ -20,7 +20,7 @@ from PyQt5.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")
NAMES = ("zoom_fit", "box_zoom", "latest", "settings", "reset_view")
ICON_SIZE = 16
_PEN_WIDTH = 1.4
@@ -93,11 +93,49 @@ def _paint_settings(painter: QPainter, size: int) -> None:
)
def _paint_reset_view(painter: QPainter, size: int) -> None:
"""A circular arrow: put the view back the way the asset framed it."""
import math
centre = size / 2.0
radius = size * 0.30
# Three quarters of a circle, drawn as short chords: Qt's arc API needs a rect and
# sixteenths of a degree, and the gap is what makes it read as "again".
start = -0.30 * math.pi
end = 1.45 * math.pi
steps = 18
previous = None
for index in range(steps + 1):
angle = start + (end - start) * index / steps
point = (
centre + radius * math.cos(angle),
centre + radius * math.sin(angle),
)
if previous is not None:
painter.drawLine(
int(round(previous[0])), int(round(previous[1])),
int(round(point[0])), int(round(point[1])),
)
previous = point
# The head sits at the open end of the arc, pointing the way the arrow runs.
head = size * 0.20
tip_x, tip_y = previous
painter.drawLine(
int(round(tip_x)), int(round(tip_y)),
int(round(tip_x - head)), int(round(tip_y - head * 0.2)),
)
painter.drawLine(
int(round(tip_x)), int(round(tip_y)),
int(round(tip_x - head * 0.2)), int(round(tip_y + head)),
)
_PAINTERS = {
"zoom_fit": _paint_zoom_fit,
"box_zoom": _paint_box_zoom,
"latest": _paint_latest,
"settings": _paint_settings,
"reset_view": _paint_reset_view,
}
@@ -16,8 +16,16 @@ from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Optional
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QFrame, QLabel, QStackedLayout, QVBoxLayout, QWidget
from PyQt5.QtCore import QSize, Qt, pyqtSignal
from PyQt5.QtWidgets import (
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QStackedLayout,
QVBoxLayout,
QWidget,
)
from orisys_app_shared.i18n import t
from orisys_app_shared.scene3d.contracts import (
@@ -32,6 +40,7 @@ from orisys_studio.scene3d.asset_source import (
find_package,
no_asset_diagnostic,
)
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
@@ -71,9 +80,12 @@ class Mesh3dPanel(QFrame):
#: from this, so no packet is adapted while the pane is off.
enabledChanged = pyqtSignal(bool)
def __init__(self, parent: Optional[QWidget] = None) -> None:
def __init__(
self, parent: Optional[QWidget] = 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
@@ -99,9 +111,31 @@ class Mesh3dPanel(QFrame):
root = QVBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
# The header carries the title and the two actions that belong to *looking*:
# put the view back to the asset's own framing, and open the settings. Everything
# else the pane can be told lives in that dialog, so the picture is the pane.
header = QHBoxLayout()
header.setContentsMargins(8, 4, 8, 0)
header.setSpacing(4)
self.title = QLabel()
self.title.setObjectName("panelTitle")
root.addWidget(self.title)
header.addWidget(self.title)
header.addStretch(1)
self.btn_reset_view = QPushButton()
self.btn_reset_view.setProperty("compact", "true")
self.btn_reset_view.setProperty("iconOnly", "true")
self.btn_reset_view.setCursor(Qt.PointingHandCursor)
self.btn_reset_view.clicked.connect(self.reset_view)
header.addWidget(self.btn_reset_view)
self.btn_settings = QPushButton()
self.btn_settings.setProperty("compact", "true")
self.btn_settings.setProperty("iconOnly", "true")
self.btn_settings.setCursor(Qt.PointingHandCursor)
self.btn_settings.clicked.connect(self.open_settings)
header.addWidget(self.btn_settings)
root.addLayout(header)
self._stack = QStackedLayout()
root.addLayout(self._stack, 1)
@@ -112,8 +146,39 @@ class Mesh3dPanel(QFrame):
self.message.setWordWrap(True)
self._stack.addWidget(self.message)
self._apply_icons()
self.retranslate()
# ------------------------------------------------------------------ actions
def open_settings(self) -> bool:
"""Show the 3D settings dialog. ``False`` when there is nothing to configure.
The pane builds the view on first use, so pressing the gear is also what makes
the 3D view exist — the same path the View menu's 「三维网格」 takes.
"""
if self._view is None:
self.ensure_view()
if self._view is None:
return False
return bool(self._view.open_settings())
def reset_view(self) -> None:
"""Go back to the framing the asset itself calls its own."""
if self._view is not None:
self._view.reset_view()
def set_theme(self, theme: str) -> None:
"""Re-tint the header icons: they are painted from the theme's own colours."""
self._theme = str(theme)
self._apply_icons()
def _apply_icons(self) -> None:
size = QSize(icons.ICON_SIZE, icons.ICON_SIZE)
self.btn_reset_view.setIcon(icons.icon("reset_view", self._theme))
self.btn_reset_view.setIconSize(size)
self.btn_settings.setIcon(icons.icon("settings", self._theme))
self.btn_settings.setIconSize(size)
# ------------------------------------------------------------------ scene
def set_sensor(
self,
@@ -400,5 +465,7 @@ class Mesh3dPanel(QFrame):
def retranslate(self) -> None:
self.title.setText(t("panels.mesh3d"))
self.btn_reset_view.setToolTip(t("panels.mesh3d_reset_tip"))
self.btn_settings.setToolTip(t("panels.mesh3d_settings_tip"))
self.setToolTip(t("panels.mesh3d_empty_tip"))
self._render_message()
+5 -3
View File
@@ -208,14 +208,16 @@ class ViewLayoutController(QObject):
self._cal_panel.setMinimumWidth(self._min_panel_widths["cal"])
self._set_check(self._actions.finger_3d if self._actions else None, show_3d, enabled=is_finger)
self._set_check(self._actions.cal_panel if self._actions else None, show_cal)
# Panel weights: the charts are the widest panel in every view, and the 2D
# panel is deliberately the smaller one.
# Panel weights: the charts are the widest panel in a view without 3D, the 3D
# pane is the widest one when it is on (it is the subject, not an inset), and
# the 2D panel is deliberately the smaller one.
arrow_weight = 3
plots_weight = 4
mesh_weight = 5
for i in range(self._splitter.count()):
widget = self._splitter.widget(i)
if widget is self._mesh_panel:
self._splitter.setStretchFactor(i, 2 if show_3d else 0)
self._splitter.setStretchFactor(i, mesh_weight if show_3d else 0)
elif widget is self._image_panel:
self._splitter.setStretchFactor(i, arrow_weight if show_arrow else 0)
elif widget is self._charts: