阶段 3:实时数据 —— 采样、合成、最新帧调度与适配器 - What: sampling.py 实现 SurfaceSampler(向量化四点双线性:无效 tap 不计入也不致无效、任一**正权** tap 无效即整顶点无效且值为 NaN、域/形状不符先拒;采样表只在加载时构建)与 remap_to_domain(按 unroll 映射 gather,空洞保持 NaN 且 valid=False,保留尾随轴以支持矢量场)。composer.py 实现 SceneComposer.compose(域校验 → 采样 → 位移 → **从形变后位置重算法线**;scalar 绝不成以 display_gain;approximate 位移产生 display_illustrative(info) 诊断、calibrated 不产生、无位移则着色并报 field_unavailable)与 build_rest_frame(参考帧:scalar 全 NaN、valid 全假、descriptor=unavailable/display_only),gl_backend.rest_surface 改为委托它,避免两处定义。coordinator.py 实现完整调度:容量 1 双 mailbox、generation/来源/会话三重过期拒绝并计数、**最多一次合成在飞行**、可见性挂起/恢复、设置变更(倍率变化重合成**屏幕上那一帧**)、close 幂等;submit 只做身份与形状的廉价校验,绝不等显示。adapters.py 实现 Finger/Box 适配器:|flow| 或 depth 响应、可选经 unroll 映射 remap、设备 valid_mask 优先、域不符时**发布空**并报出所见的两个形状;一律不造位移(未标定的毫米不如没有),并新增 display_illustrative 到诊断码表(info)。assets.py 从"桩类堆"改成门面:re-export 真正实现 + SceneCompiler.compile(外壳 GLB 与预算检查;加密请求明确拒绝并指向阶段 5)。测试:shared/tests/test_scene3d_live.py 25 项(采样/无效语义/倍率只影响位置/示意形变标注/法线重算/remap 空洞/参考帧/只读数组)、apps/studio/tests/test_scene3d_coordinator.py 13 项(状态机、过期、替换计数、单飞行、submit p99<1ms、切换场景丢弃在飞结果、隐藏挂起、倍率重合成、close 幂等)、test_scene3d_adapters.py 13 项;并把三处"桩仍未实现"的过时断言改成对已实现行为的断言。docs/architecture/studio-3d-stages.md 阶段 3 记为完成。 - Why: 阶段 3 决定"屏幕上的颜色和力值到底对应什么"。三条最容易错且肉眼看不出来的规则必须由代码而不是约定来保证:无效点不是 0(NaN 与 valid 分离,否则缺口会被画成一个测量值)、显示倍率只动位置不动测量(否则色标悄悄变了含义)、以及显示永远不能给采集施加背压(容量 1 mailbox + 单飞行合成 + 廉价 submit)。 - Verification: shared 全量通过(scene3d 相关 103 项:contracts 26、assets 33、package 19、live 25);studio 全量通过(scene3d 相关 54 项:stage1 24、skeleton 4、coordinator 13、adapters 13;GL 冒烟 10 项在真实 context 下另行通过);ui_check 554 项 0 失败。过程中修掉 3 个真实缺陷:倍率变化时用 incoming.peek() 取不到"屏幕上那一帧"(已被消费)导致改了倍率画面不动——改为保留 _last_field_frame;display_illustrative 不在码表里被默认成 error;assets.py 里与 package/legacy 同名的重复桩类(会误导集成者,已改为门面)。另修正两处我自己的错误断言(对称凸起顶点法线本就竖直;pytest.approx 不能直接 .all())。 - Not in this change: 手势与面板(阶段 4);示意位移的真实来源(需要 SDK lift + 已确认尺度,现阶段 adapter 一��� displacement=None);app_window 接线与记录隔离(阶段 4)。
This commit is contained in:
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -397,18 +398,51 @@ def test_the_contract_layer_imports_without_qt_or_opengl():
|
||||
)
|
||||
|
||||
|
||||
def test_the_cpu_skeleton_is_importable_and_says_which_stage_it_needs():
|
||||
from orisys_app_shared.scene3d import assets, composer, sampling
|
||||
def test_the_cpu_layer_is_importable_and_works_without_a_gpu():
|
||||
"""The acquisition thread imports this package; it must not drag Qt or GL in.
|
||||
|
||||
with pytest.raises(NotImplementedError) as sampler:
|
||||
sampling.SurfaceSampler().sample(_frame(), _binding())
|
||||
assert "stage 3" in str(sampler.value)
|
||||
with pytest.raises(NotImplementedError) as composer_error:
|
||||
composer.SceneComposer().rest_frame(_assets(), c.FrameIdentity("live-A", 1, 0, 0.0, 1))
|
||||
assert "stage 3" in str(composer_error.value)
|
||||
with pytest.raises(NotImplementedError) as loader:
|
||||
assets.PackagedAssetLoader().load(None, None) # type: ignore[arg-type]
|
||||
assert "stage 2" in str(loader.value)
|
||||
Stages 2 and 3 filled in the CPU pieces, so this checks they are reachable and
|
||||
usable here — the sampler and the composer run on a machine with no GPU, which
|
||||
is the whole reason the contracts layer is separate from the view.
|
||||
"""
|
||||
from orisys_app_shared.scene3d import assets, composer, sampling, synthetic
|
||||
|
||||
scene = synthetic.poc_scene()
|
||||
frame = _frame()
|
||||
fields = sampling.sample_field(frame, _binding())
|
||||
assert np.asarray(fields.scalar).shape == (4,)
|
||||
composed = composer.SceneComposer().compose(
|
||||
scene, c.SurfaceFieldFrame(
|
||||
identity=frame.identity,
|
||||
geometry_id=scene.geometry_id,
|
||||
field_domain_id=scene.field_domain_id,
|
||||
scalar=c.freeze(np.zeros(scene.shape_hw, dtype=np.float32), dtype=np.float32),
|
||||
scalar_descriptor=c.FieldDescriptor("flow_magnitude", "px", "approximate", False),
|
||||
valid=c.freeze(np.ones(scene.shape_hw, dtype=bool), dtype=np.bool_),
|
||||
),
|
||||
c.DisplaySettings(),
|
||||
)
|
||||
assert composed.scalar.shape == (9,)
|
||||
# The loading facade is real too (stage 2), and the compiler refuses what it
|
||||
# cannot yet do rather than writing something unprotected.
|
||||
assert callable(assets.PackagedAssetLoader().load)
|
||||
with pytest.raises(c.ThreeDError) as excinfo:
|
||||
assets.SceneCompiler().compile(
|
||||
scene,
|
||||
None,
|
||||
Path("unused.oryscene"),
|
||||
c.CompileOptions(
|
||||
asset_id="x",
|
||||
version="0",
|
||||
expected=synthetic.poc_expected(),
|
||||
transform_sensor_from_glb=np.eye(4),
|
||||
max_surface_vertices=0,
|
||||
max_housing_triangles=1000,
|
||||
signing_key_reference="build-key",
|
||||
encryption_key_id="customer-1",
|
||||
),
|
||||
)
|
||||
assert "stage 5" in excinfo.value.diagnostic.message
|
||||
|
||||
|
||||
def test_summarize_parts_counts_vertices_per_part():
|
||||
|
||||
Reference in New Issue
Block a user