feat(studio): 三维视图接入产品(阶段 4:界面、生命周期与记录隔离)
- What: `Scene3DView` 装配(widget/相机/渲染器/合成器/协调器/适配器),`asset_source` 只查找并后台加载已编译的 `.oryscene` 包,`scene3d/prefs` 持久化并**修复**显示设置, `ui/mesh3d_panel` 用真实容器替换占位标签(无 GL/无包/包被拒/传感器不匹配各显示一行原因), `main.py` 在 `QApplication` 之前选定 GL profile,`app_window` 接线:3D 作为采集线程上的 第二个 sink、`_on_run_tick` 驱动 `tick()`、视图切换与 `closeEvent` 释放、preferences 落盘。 - Why: 阶段 4 的验收要求"三维开关不影响采集/记录/力曲线",并把阶段 1 留下的 "开/关三维 CSV 一致"这条补上;同时把"没有 context"从"看起来像在工作"变成可显示的终态。 - Verification: app 侧 test_scene3d_view 28 项、test_scene3d_gl_smoke 14 项(真实 GPU)、 adapters 17 项、skeleton 4 项、stage1 24 项、coordinator 13 项、ui_smoke 86 项; shared 侧 contract 26 / assets 33 / package 19 / live 25;`ui_check.py` 566 项检查 0 失败 (新增 "3D pane" 段 12 项);`locale_check.py` 302 个字面 key 0 缺失、中英各 377 key 对齐; 记录隔离:同一 96 帧会话挂/不挂 3D 消费者,`forces.csv` 逐字节相同、有效样本数与记录问题数相同; 生产者预算:合成器放慢到 50 ms 时 `submit_packet` p99 < 1 ms。 - Not in this change: 示意位移(`displacement_sensor_m`)仍未接入(需 SDK lift 与已确认尺度); ARM 板卡适配(按用户要求暂缓);包签名/AES-GCM 与长时性能报告(阶段 5); 仓库中仍未提供 `.oryscene` 包(发布产物,且其单位与变换必须由人声明)。
This commit is contained in:
@@ -158,11 +158,116 @@ def test_the_finger_adapter_can_be_built_from_a_domain(scene):
|
||||
assert domain.source_hw == (288, 512), domain.source_hw
|
||||
|
||||
|
||||
def test_the_shipped_map_cannot_be_applied_to_the_shipped_flow():
|
||||
"""Why the app must not remap the finger's flow: the map is domain→*source*.
|
||||
|
||||
The unroll map is the same size as the UV domain, but its values are pixel
|
||||
coordinates in the 512x288 source image — not in the 300x280 domain. The live
|
||||
flow is already computed on the unrolled frame, so feeding it through this map
|
||||
would be a double remap: it would index the flow at source-pixel coordinates.
|
||||
This test pins the reason the app passes no map, so a later "improvement" has to
|
||||
argue with it.
|
||||
|
||||
Note the asymmetry that makes the mistake look plausible: in x the map exceeds
|
||||
the domain's column count (so it would read out of range), while in y it stays
|
||||
below the row count (so it would silently read the wrong rows). Half of a
|
||||
double remap crashes and half of it lies.
|
||||
"""
|
||||
map_path = _run_map()
|
||||
if map_path is None:
|
||||
pytest.skip("the shipped unroll map is not present")
|
||||
from orisys_app_shared.scene3d.legacy import load_legacy_domain
|
||||
|
||||
domain = load_legacy_domain(map_path)
|
||||
assert domain.map_x is not None and domain.map_y is not None
|
||||
rows, columns = domain.shape_hw
|
||||
source_rows, source_columns = domain.source_hw
|
||||
assert (rows, columns) == (300, 280) and (source_rows, source_columns) == (288, 512)
|
||||
|
||||
# Bounded by the source, not by the domain: that is what makes them source pixels.
|
||||
assert 0.0 <= float(domain.map_x[domain.map_x >= 0].max()) <= source_columns - 1
|
||||
assert 0.0 <= float(domain.map_y[domain.map_y >= 0].max()) <= source_rows - 1
|
||||
assert float(domain.map_x.max()) > columns - 1, (
|
||||
"x would index past the last column of a 280-wide flow field"
|
||||
)
|
||||
assert float(domain.map_y.max()) > rows - 1 or source_rows <= rows, (
|
||||
"y stays inside the 300-row field: the y half of the mistake is silent"
|
||||
)
|
||||
|
||||
|
||||
def test_the_live_flow_is_already_the_scene_domain():
|
||||
"""The measurement behind that decision, taken from the shipping config.
|
||||
|
||||
``camera.resolution_stitch`` is what ``Sensor`` sizes its flow buffer from and
|
||||
what the unroll stage outputs, so the flow grid *is* the UV domain. If a future
|
||||
config changes that, this test fails and the adapter wiring has to be revisited
|
||||
rather than the numbers quietly not matching.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
config = Path(__file__).resolve().parents[1] / "config" / "sensors" / "finger"
|
||||
map_path = _run_map()
|
||||
if not (config / "sensor.json").is_file() or map_path is None:
|
||||
pytest.skip("the finger config and the unroll map are needed")
|
||||
camera = json.loads((config / "sensor.json").read_text(encoding="utf-8"))["camera"]
|
||||
resolution = camera["resolution_stitch"]
|
||||
# Sensor builds `flow` as (resolution_stitch[1], resolution_stitch[0]).
|
||||
flow_hw = (int(resolution[1]), int(resolution[0]))
|
||||
from orisys_app_shared.scene3d.legacy import load_legacy_domain
|
||||
|
||||
domain = load_legacy_domain(map_path)
|
||||
assert flow_hw == tuple(domain.shape_hw) == (300, 280)
|
||||
|
||||
|
||||
def test_the_finger_adapter_does_not_colour_outside_the_uv_island(scene):
|
||||
"""The device mask is what says where the surface is; outside it there is none."""
|
||||
adapter = FingerFrameAdapter(
|
||||
geometry_id=scene.geometry_id,
|
||||
field_domain_id=scene.field_domain_id,
|
||||
shape_hw=scene.shape_hw,
|
||||
)
|
||||
flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32)
|
||||
flow[..., 0] = 1.0
|
||||
mask = np.zeros(scene.shape_hw, dtype=bool)
|
||||
mask[1, 1] = True
|
||||
frame = adapter.snapshot(
|
||||
FakePacket(flow=flow, valid_mask=mask), runtime=None, identity=_identity()
|
||||
)
|
||||
assert frame is not None
|
||||
valid = np.asarray(frame.valid)
|
||||
assert valid.sum() == 1 and bool(valid[1, 1])
|
||||
assert np.asarray(frame.scalar)[0, 0] == 1.0, (
|
||||
"the scalar is kept as measured; only validity says it is not a measurement"
|
||||
)
|
||||
|
||||
|
||||
def test_a_device_mask_from_another_grid_is_ignored_not_stretched(scene):
|
||||
adapter = FingerFrameAdapter(
|
||||
geometry_id=scene.geometry_id,
|
||||
field_domain_id=scene.field_domain_id,
|
||||
shape_hw=scene.shape_hw,
|
||||
)
|
||||
flow = np.ones((*scene.shape_hw, 2), dtype=np.float32)
|
||||
wrong = np.zeros((7, 7), dtype=bool)
|
||||
frame = adapter.snapshot(
|
||||
FakePacket(flow=flow, valid_mask=wrong), runtime=None, identity=_identity()
|
||||
)
|
||||
assert frame is not None
|
||||
assert np.asarray(frame.valid).all()
|
||||
|
||||
|
||||
def _run_map():
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(r"D:\orisys_ws\orisys-sdk\sensors\finger\assets\30fps-0703\unroll_map_uv_s0.4.npz")
|
||||
return path if path.is_file() else None
|
||||
root = Path(__file__).resolve().parents[1] / "config" / "sensors" / "finger" / "assets"
|
||||
for name in ("30fps-0703", "visualization"):
|
||||
path = root / name / "unroll_map_uv_s0.4.npz"
|
||||
if path.is_file():
|
||||
return path
|
||||
for path in sorted(root.glob("*/unroll_map_uv*.npz")):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- box adapter
|
||||
|
||||
Reference in New Issue
Block a user