界面:三维面板只留画面与图例,全部配置进「三维显示设置」对话框;三维占比提为最宽
- 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:
@@ -147,6 +147,112 @@ def _run_scene(window, app, scene: str):
|
||||
app.processEvents()
|
||||
|
||||
|
||||
def _flow_scene(window, app, *, frames: int = 240) -> None:
|
||||
"""Drive a synthetic session whose frames carry a real optical-flow field.
|
||||
|
||||
The 3D pane is coloured by the frame's *own* flow (see
|
||||
``orisys_studio.scene3d.adapters``): a packet that does not state one is refused,
|
||||
so a picture with colour needs packets that do. This is the scene the interface is
|
||||
reviewed from — the real widgets, the real renderer, the real adapter, and a field
|
||||
shaped like a pressed fingertip (strongest at the centre, fading to the rim).
|
||||
"""
|
||||
import time as _time
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.acquisition.sdk_worker import SdkPacket
|
||||
|
||||
from ui_check import _StuckWorker
|
||||
|
||||
stuck = _StuckWorker()
|
||||
window._ensure_workers = lambda: None
|
||||
window._box_worker = stuck
|
||||
window._finger_worker = stuck
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
|
||||
# The unrolled finger domain: 280x300, which is the grid the scene's UV island
|
||||
# uses. The flow is already unrolled, so the adapter is given no remap.
|
||||
height, width = 300, 280
|
||||
grid_y, grid_x = np.mgrid[0:height, 0:width].astype(np.float32)
|
||||
centre_y, centre_x = height * 0.52, width / 2.0
|
||||
dy, dx = grid_y - centre_y, grid_x - centre_x
|
||||
radius = np.sqrt(dx * dx + dy * dy) + 1e-3
|
||||
profile = np.exp(-((radius / (width * 0.30)) ** 2)).astype(np.float32)
|
||||
flow = np.stack(
|
||||
[dx / radius * profile * 7.0, dy / radius * profile * 7.0], axis=-1
|
||||
).astype(np.float32)
|
||||
# A recognisable camera image as well, so the 2D panel is not black next to it.
|
||||
frame = np.zeros((height, width, 3), np.uint8)
|
||||
frame[..., 1] = (40.0 + profile * 150.0).astype(np.uint8)
|
||||
frame[..., 2] = (profile * 90.0).astype(np.uint8)
|
||||
|
||||
for index in range(frames):
|
||||
# The forces move: a flat line reads as "the demo is not running", and the
|
||||
# charts are half of what the video has to show.
|
||||
phase = index / 30.0
|
||||
# A period of a couple of seconds, so the trace visibly moves inside the chart's
|
||||
# rolling window instead of looking like a slow ramp.
|
||||
normal = 3.4 + 2.4 * math.sin(phase * 2.4)
|
||||
shear_x = 1.6 * math.sin(phase * 3.1 + 0.4)
|
||||
shear_y = -1.3 * math.cos(phase * 2.7)
|
||||
stuck.emit(
|
||||
SdkPacket(
|
||||
raw_frame=frame,
|
||||
processed_frame=frame,
|
||||
flow=flow,
|
||||
timestamp=1000.0 + index / 30.0,
|
||||
mono_t=float(index) / 30.0,
|
||||
fps=30.0,
|
||||
process_ms=6.0,
|
||||
capture_fps=30.0,
|
||||
session_id=1,
|
||||
frame_id=index,
|
||||
valid=True,
|
||||
raw_available=True,
|
||||
normal_force=normal,
|
||||
shear_x=shear_x,
|
||||
shear_y=shear_y,
|
||||
normal_force_raw=9.0 + index * 0.1,
|
||||
shear_x_raw=0.4,
|
||||
shear_y_raw=-0.4,
|
||||
force_unit="N",
|
||||
force_calibrated=True,
|
||||
calibrated_axes=("fn", "fx", "fy"),
|
||||
flow_valid=True,
|
||||
flow_frame_id=index,
|
||||
depth_valid=None,
|
||||
depth_frame_id=None,
|
||||
forces_valid=True,
|
||||
)
|
||||
)
|
||||
# The window's own presenter runs *while* the frames arrive, the way it does in
|
||||
# a session: the charts then hold a real time series instead of one sample.
|
||||
if index % 4 == 0:
|
||||
window._on_run_tick()
|
||||
app.processEvents()
|
||||
|
||||
# The 3D picture is composed on the view's own worker and taken one finished frame
|
||||
# at a time by the run timer, so pump both until something has actually been shown.
|
||||
view = window.mesh3d_panel.view
|
||||
deadline = _time.perf_counter() + 20.0
|
||||
while _time.perf_counter() < deadline:
|
||||
window._on_run_tick()
|
||||
app.processEvents()
|
||||
if view is not None and view.render_totals().get("shown", 0) > 0:
|
||||
break
|
||||
_time.sleep(0.01)
|
||||
window._run_timer.stop()
|
||||
# The overview strip is rebuilt at most once a second (it is a whole-session
|
||||
# decimation) and this scene fills eight seconds of session in well under that, so
|
||||
# it is rebuilt once here — the same thing ``_demo_data`` does for the same reason.
|
||||
window.force_plots._overview_dirty = True
|
||||
window.force_plots._last_overview_at = 0.0
|
||||
window.force_plots._sync_overview()
|
||||
for _ in range(6):
|
||||
app.processEvents()
|
||||
|
||||
|
||||
def _demo_data(window) -> None:
|
||||
"""Fill the plots, the calibration table and the arrow canvas."""
|
||||
from orisys_app_shared.force.session import SessionSample, SessionStore
|
||||
@@ -242,14 +348,16 @@ def main() -> int:
|
||||
parser.add_argument("--demo", action="store_true", help="inject demo plot/table data")
|
||||
parser.add_argument(
|
||||
"--scene",
|
||||
choices=("idle", "connecting", "recording", "replay"),
|
||||
choices=("idle", "connecting", "recording", "replay", "flow3d"),
|
||||
default="idle",
|
||||
help=(
|
||||
"connecting renders the Stop/cancel state of a hung device open; "
|
||||
"recording renders the record strip of a running recording "
|
||||
"(mocked stats: no file is written, no device is opened); "
|
||||
"replay writes one short synthetic recording and opens it in the "
|
||||
"replay view so the progress bar can be reviewed"
|
||||
"replay view so the progress bar can be reviewed; "
|
||||
"flow3d drives packets that carry a real optical-flow field, which is "
|
||||
"what colours the 3D pane (use with --sensor finger --panels 3d)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -317,7 +425,10 @@ def main() -> int:
|
||||
if args.scene == "replay":
|
||||
_run_scene(window, app, "replay")
|
||||
|
||||
if args.demo and args.scene != "replay":
|
||||
if args.scene == "flow3d":
|
||||
_flow_scene(window, app)
|
||||
|
||||
if args.demo and args.scene not in ("replay", "flow3d"):
|
||||
_demo_data(window)
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
|
||||
Reference in New Issue
Block a user