阶段 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:
ykcheungab
2026-09-14 15:21:39 +08:00
parent d0dcac743f
commit ea1027e5a6
14 changed files with 2036 additions and 198 deletions
+182 -10
View File
@@ -8,44 +8,216 @@ The constraints here are about *where* the data may be read, not about optics:
runtime, because the SDK's arrays are that thread's working memory;
* it copies what the frame needs and publishes it read-only — the GUI thread must
never reach back into a live runtime;
* it copies **nothing** when the three-dimensional view is not subscribed to, and
returns ``None`` when the frame carries no usable field. A frame is never
assembled from two different acquisitions, and a missing field is never filled
in from the previous one.
* it publishes **nothing** when the frame carries no usable field, or when the
field is on a different grid than the scene's UV domain. A remap that is not
configured is not guessed at: the adapter says which shapes it saw, so the
mismatch is a fact in the diagnostics rather than a picture that looks plausible.
Two things the v1 adapters deliberately do **not** do:
* they do not invent a displacement. The SDK's lift uses optical flow and a scale
estimated from the mesh, which the design document calls "not a verified
displacement calibration"; a millimetre field that was never calibrated is worse
than no field, so ``displacement_sensor_m`` is ``None`` and the surface is
coloured in place. The composer already handles an ``approximate`` displacement
for when a verified one arrives;
* they do not rescale or pad a field to fit. A field whose shape does not match the
domain is a mismatch (`field_domain_mismatch`), not something to resize.
"""
from __future__ import annotations
from typing import Any, Optional
import numpy as np
from orisys_app_shared.acquisition.models import SdkPacket
from orisys_app_shared.scene3d.contracts import (
Diagnostic,
ExpectedGeometry,
FieldDescriptor,
FrameIdentity,
SurfaceFieldFrame,
diagnostic,
error,
freeze,
)
from orisys_app_shared.scene3d.sampling import remap_to_domain
#: What the adapters can colour: the optical flow's magnitude, in pixels. An
#: unsigned magnitude is a sequential colour scale, and ``approximate`` says the
#: number is a pixel displacement rather than a calibrated length.
FLOW_MAGNITUDE = FieldDescriptor(
quantity="flow_magnitude", unit="px", quality="approximate", signed=False
)
#: The deformation response the SDK's depth map reports: a relative response, not a
#: length. It is only ever coloured, never turned into millimetres.
DEPTH_RESPONSE = FieldDescriptor(
quantity="depth_response", unit="1", quality="approximate", signed=False
)
_STAGE = "stage 3 (live data)"
def flow_magnitude(packet: SdkPacket) -> Optional[np.ndarray]:
"""``|flow|`` as float32, or ``None`` when the packet carries no flow."""
flow = getattr(packet, "flow", None)
if flow is None:
return None
array = np.asarray(flow, dtype=np.float32)
if array.ndim == 3 and array.shape[-1] == 2:
return np.sqrt(array[..., 0] ** 2 + array[..., 1] ** 2)
if array.ndim == 2:
return array
return None
def _shape_of(array: Optional[np.ndarray]) -> str:
return "none" if array is None else "x".join(str(int(v)) for v in array.shape[:2])
class FingerFrameAdapter:
"""The finger runtime already computes a UV field; copy it, with its identity."""
"""The finger runtime's field, in the scene's UV domain.
``map_x``/``map_y`` are the unroll map's source coordinates. When they are
supplied the field is remapped through them; when they are not, the field is
used directly and only if its shape already *is* the domain.
"""
def __init__(
self,
*,
geometry_id: str,
field_domain_id: str,
shape_hw: tuple[int, int],
map_x: Optional[np.ndarray] = None,
map_y: Optional[np.ndarray] = None,
) -> None:
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.map_x = map_x
self.map_y = map_y
self.last_diagnostic: Optional[Diagnostic] = None
@classmethod
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,
field_domain_id=str(getattr(domain, "domain_id")),
shape_hw=tuple(getattr(domain, "shape_hw")),
map_x=getattr(domain, "map_x", None),
map_y=getattr(domain, "map_y", None),
)
def snapshot(
self, packet: SdkPacket, runtime: Any, identity: FrameIdentity
) -> Optional[SurfaceFieldFrame]:
raise NotImplementedError(f"{_STAGE}: snapshot the finger UV field")
magnitude = flow_magnitude(packet)
if magnitude is None:
self.last_diagnostic = diagnostic(
"field_unavailable", "this frame carries no optical flow"
)
return None
if self.map_x is not None and self.map_y is not None:
try:
values, valid = remap_to_domain(
magnitude, map_x=self.map_x, map_y=self.map_y, shape_hw=self.shape_hw
)
except Exception as exc: # noqa: BLE001 - a bad map is a diagnostic
self.last_diagnostic = getattr(exc, "diagnostic", None) or diagnostic(
"field_domain_mismatch", f"remap failed: {type(exc).__name__}: {exc}"
)
return None
scalar = values
mask = valid
elif tuple(magnitude.shape[:2]) == self.shape_hw:
scalar = freeze(magnitude, dtype=np.float32)
mask = freeze(np.isfinite(magnitude), dtype=np.bool_)
else:
# Say what was seen: this is the message that tells an engineer whether
# the flow arrives in the tracker's grid or already unrolled.
self.last_diagnostic = diagnostic(
"field_domain_mismatch",
"the flow is not in the scene's UV domain and no remap is configured",
flow=_shape_of(magnitude),
domain=f"{self.shape_hw[0]}x{self.shape_hw[1]}",
)
return None
self.last_diagnostic = None
return SurfaceFieldFrame(
identity=identity,
geometry_id=self.geometry_id,
field_domain_id=self.field_domain_id,
scalar=scalar,
scalar_descriptor=FLOW_MAGNITUDE,
valid=mask,
)
class BoxFrameAdapter:
"""Box/mini: a scalar field only.
"""Box/mini: a scalar field only, and only when the grid already matches.
There is no verified spatial scale for these sensors yet, so no displacement is
There is no verified spatial scale for these sensors, so no displacement is
invented: a millimetre height field that was never calibrated is worse than no
height field at all.
"""
def __init__(
self,
*,
geometry_id: str,
field_domain_id: str,
shape_hw: tuple[int, int],
) -> None:
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
def snapshot(
self, packet: SdkPacket, identity: FrameIdentity, domain: ExpectedGeometry
) -> Optional[SurfaceFieldFrame]:
raise NotImplementedError(f"{_STAGE}: snapshot the box scalar field")
if tuple(domain.shape_hw) != self.shape_hw:
self.last_diagnostic = diagnostic(
"field_domain_mismatch",
"the device description names a different field domain",
device=f"{domain.shape_hw[0]}x{domain.shape_hw[1]}",
scene=f"{self.shape_hw[0]}x{self.shape_hw[1]}",
)
return None
candidates = (
("depth_response", getattr(packet, "depth", None)),
("flow_magnitude", flow_magnitude(packet)),
)
for name, field in candidates:
if field is None:
continue
array = np.asarray(field, dtype=np.float32)
if array.ndim != 2 or tuple(array.shape) != self.shape_hw:
continue
mask = np.isfinite(array)
explicit = getattr(packet, "valid_mask", None)
if explicit is not None:
explicit_array = np.asarray(explicit, dtype=bool)
if tuple(explicit_array.shape) == self.shape_hw:
# The device's own mask wins: it knows where its surface is.
mask = mask & explicit_array
self.last_diagnostic = None
return SurfaceFieldFrame(
identity=identity,
geometry_id=self.geometry_id,
field_domain_id=self.field_domain_id,
scalar=freeze(array, dtype=np.float32),
scalar_descriptor=(
DEPTH_RESPONSE if name == "depth_response" else FLOW_MAGNITUDE
),
valid=freeze(mask, dtype=np.bool_),
)
self.last_diagnostic = diagnostic(
"field_unavailable",
"no usable scalar field of the scene's shape in this frame",
depth=_shape_of(getattr(packet, "depth", None)),
flow=_shape_of(flow_magnitude(packet)),
domain=f"{self.shape_hw[0]}x{self.shape_hw[1]}",
)
return None
@@ -1,32 +1,50 @@
"""Frame schedule and state machine for the three-dimensional view.
Stage 3 (mailboxes, compose worker) and stage 4 (Qt timer, visibility, settings) in
Stage 3 (mailboxes, compose worker) and stage 4 (Qt timer, visibility) in
``docs/architecture/studio-3d-stages.md``.
The whole point of this class is that **the acquisition loop never waits for the
display**. Producers call :meth:`submit`, which validates cheaply and drops the
previous un-consumed frame; a GUI timer calls :meth:`tick`, which picks up the
newest composed frame and repaints. Display drops are counted here and never in
the recording channel, and no CPU task is submitted while one is still running.
display**. A producer calls :meth:`submit`, which validates cheaply and drops the
previous un-consumed frame; a GUI timer calls :meth:`tick`, which hands the newest
field frame to the composer, picks up the newest composed frame and asks the widget
to repaint. Three properties make that true rather than aspirational:
* both mailboxes hold **one** frame, and ``submit`` only ever swaps a reference
under a lock — the work it can do is bounded by the lock, not by the display;
* **at most one compose task is in flight**. A slow composer makes frames drop; it
cannot make the queue grow;
* display drops are counted here and never mixed into the recording channel, which
keeps running through the existing sink path untouched.
Staleness is about *identity*, not timing: a frame from another generation, another
session or another source than the active scene is dropped with ``stale_frame`` and
counted, and an old result can never overwrite a new scene because the composer's
result carries the identity it was built from and is checked again on arrival.
"""
from __future__ import annotations
from concurrent.futures import Executor
from typing import Optional
import threading
from concurrent.futures import Executor, Future
from typing import Any, Optional
from orisys_app_shared.scene3d.contracts import (
Diagnostic,
DisplaySettings,
FrameIdentity,
LatestFrameMailbox,
OfferResult,
RenderFrame,
SceneAssets,
SceneState,
SurfaceFieldFrame,
diagnostic,
error,
)
from orisys_studio.scene3d.widget import Sensor3DWidget
_STAGE = "stage 3 (schedule) and stage 4 (Qt integration)"
#: How many composed frames may be "lost" before the coordinator stops caring: the
#: counter is informational, never a reason to throttle acquisition.
MAX_INFLIGHT_COMPOSES = 1
class ThreeDCoordinator:
@@ -34,48 +52,41 @@ class ThreeDCoordinator:
def __init__(
self,
widget: Sensor3DWidget,
composer: object,
widget: Any,
composer: Any,
executor: Executor,
max_fps: int = 30,
) -> None:
self._widget = widget
self._composer = composer
self._executor = executor
self._max_fps = int(max_fps)
self._max_fps = max(1, int(max_fps))
self._incoming: LatestFrameMailbox[SurfaceFieldFrame] = LatestFrameMailbox()
self._composed: LatestFrameMailbox[RenderFrame] = LatestFrameMailbox()
self._lock = threading.Lock()
self._state: SceneState = "disabled"
self._generation = 0
self._assets: Optional[SceneAssets] = None
self._source_id = ""
self._session_id: Optional[int] = None
self._visible = False
self._settings = DisplaySettings(max_fps=self._max_fps)
self._inflight: Optional[Future] = None
self._stale_frames = 0
self._invalid_frames = 0
self._last_diagnostic: Optional[Diagnostic] = None
self._closed = False
self._settings_dirty = False
self._last_composed_identity: Optional[FrameIdentity] = None
#: 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
def activate(self, assets: SceneAssets, source_id: str) -> int:
"""Adopt a scene and a data source; returns the new generation."""
raise NotImplementedError(f"{_STAGE}: activate a scene")
def submit(self, frame: SurfaceFieldFrame) -> OfferResult:
"""Offer a field frame from a producer thread. Never blocks on the GUI."""
raise NotImplementedError(f"{_STAGE}: accept or reject an incoming frame")
def tick(self) -> None:
"""GUI timer: take the newest composed frame, present it, schedule work."""
raise NotImplementedError(f"{_STAGE}: pump the mailboxes")
def set_visible(self, visible: bool) -> None:
"""Hiding suspends composing, uploading and rendering — not acquisition."""
raise NotImplementedError(f"{_STAGE}: suspend or resume the view")
def set_settings(self, settings: DisplaySettings) -> None:
raise NotImplementedError(f"{_STAGE}: apply display settings")
def close(self) -> None:
"""Cancel work, stop the timer, clear both mailboxes. Idempotent."""
raise NotImplementedError(f"{_STAGE}: shut the view down")
# ------------------------------------------------------------------- state
@property
def state(self) -> SceneState:
return self._state
with self._lock:
return self._state
@property
def generation(self) -> int:
@@ -83,7 +94,261 @@ class ThreeDCoordinator:
@property
def superseded_frames(self) -> int:
"""Frames the display never showed. Not a recording drop."""
return (
self._incoming.superseded_frames + self._composed.superseded_frames
"""Frames the display never showed. Never a recording drop."""
return self._incoming.superseded_frames + self._composed.superseded_frames
@property
def stale_frames(self) -> int:
return self._stale_frames
@property
def invalid_frames(self) -> int:
return self._invalid_frames
@property
def last_diagnostic(self) -> Optional[Diagnostic]:
return self._last_diagnostic
@property
def visible(self) -> bool:
return self._visible
def settings(self) -> DisplaySettings:
return self._settings
def _set_state(self, state: SceneState) -> None:
with self._lock:
self._state = state
# --------------------------------------------------------------- lifetime
def activate(self, assets: SceneAssets, source_id: str) -> int:
"""Adopt a scene and a data source; returns the new generation.
Everything already in flight belongs to the previous scene and is dropped:
the generation is what makes "old" mean something.
"""
if self._closed:
raise error("asset_rejected", "this coordinator has been closed")
self._cancel_inflight()
self._incoming.clear()
self._composed.clear()
self._assets = assets
self._source_id = str(source_id)
self._session_id = None
self._generation += 1
self._last_composed_identity = None
self._last_field_frame = None
self._settings_dirty = False
self._widget.set_scene(assets)
if self._visible:
self._widget.present(self._composer.rest_frame(assets, self._identity(0)))
self._set_state("ready")
else:
self._set_state("loading" if self._visible else "suspended")
return self._generation
def set_visible(self, visible: bool) -> None:
"""Hiding suspends composing, uploading and rendering — not acquisition."""
visible = bool(visible)
if self._closed:
return
self._widget.setVisible(visible)
if not visible:
self._visible = False
self._incoming.clear()
self._composed.clear()
self._cancel_inflight()
self._set_state("suspended")
return
self._visible = True
# Come back on the newest frame rather than replaying what was dropped.
self._set_state("ready" if self._assets is not None else "disabled")
# ---------------------------------------------------------------- producers
def submit(self, frame: SurfaceFieldFrame) -> OfferResult:
"""Offer a field frame from a producer thread. Never blocks on the GUI."""
if self._closed or not self._visible:
return "disabled"
with self._lock:
assets = self._assets
generation = self._generation
session_id = self._session_id
if assets is None:
return "disabled"
identity = frame.identity
if identity.source_id != self._source_id or identity.generation != generation:
self._stale_frames += 1
self._last_diagnostic = diagnostic(
"stale_frame",
"a frame from another source or generation was dropped",
source=identity.source_id,
generation=str(identity.generation),
)
return "stale"
if session_id is not None and identity.session_id != session_id:
self._stale_frames += 1
return "stale"
problem = self._reject_reason(frame, assets)
if problem is not None:
self._invalid_frames += 1
self._last_diagnostic = problem
return "invalid"
with self._lock:
if session_id is None:
self._session_id = identity.session_id
return self._incoming.offer(frame)
@staticmethod
def _reject_reason(
frame: SurfaceFieldFrame, assets: SceneAssets
) -> Optional[Diagnostic]:
"""The cheap checks a producer thread may do: identity and shape only."""
if frame.geometry_id != assets.geometry_id:
return diagnostic(
"geometry_mismatch",
"the frame belongs to a different geometry",
frame=frame.geometry_id,
scene=assets.geometry_id,
)
if frame.field_domain_id != assets.field_domain_id:
return diagnostic(
"field_domain_mismatch",
"the frame is on a different field domain",
frame=frame.field_domain_id,
scene=assets.field_domain_id,
)
if tuple(frame.scalar.shape) != tuple(assets.binding.shape_hw):
return diagnostic(
"field_domain_mismatch",
"the frame's shape does not match the scene's UV domain",
frame=f"{frame.scalar.shape[0]}x{frame.scalar.shape[1]}",
scene=f"{assets.binding.shape_hw[0]}x{assets.binding.shape_hw[1]}",
)
if tuple(frame.valid.shape) != tuple(frame.scalar.shape):
return diagnostic(
"field_domain_mismatch", "the frame's scalar and validity disagree"
)
return None
# ------------------------------------------------------------------- consumer
def tick(self) -> None:
"""GUI timer: start one compose, take the newest result, repaint."""
if self._closed or not self._visible or self._assets is None:
return
self._collect_inflight()
if self._inflight is None:
if self._settings_dirty and self._last_field_frame is not None:
# A gain change re-draws the field that is already on screen; taking
# a newer one would show a different instant than the one the
# operator just changed the exaggeration of.
self._settings_dirty = False
self._start_compose(self._last_field_frame)
else:
self._settings_dirty = False
frame = self._incoming.take()
if frame is not None:
self._start_compose(frame)
composed = self._composed.take()
if composed is not None:
if not self._identity_is_current(composed.identity):
self._stale_frames += 1
return
self._widget.set_superseded_frames(self.superseded_frames)
self._widget.present(composed)
self._last_composed_identity = composed.identity
self._set_state("live" if composed.valid.any() else "no_data")
elif self._state in ("ready", "loading"):
# Nothing has arrived yet: show the reference model rather than a blank
# window, and say that no measurement is being displayed.
if self._last_composed_identity is None:
self._widget.present(self._composer.rest_frame(self._assets, self._identity(0)))
self._set_state("no_data")
def set_settings(self, settings: DisplaySettings) -> None:
"""Apply display settings; a gain change means recomposing the last field."""
previous = self._settings
self._settings = settings
self._widget.set_settings(settings)
gain_changed = (
previous.display_gain != settings.display_gain
or previous.deform_enabled != settings.deform_enabled
)
if gain_changed and self._assets is not None:
# The scalar is untouched by the gain, but the *positions* are not, so
# the last field frame has to be composed again.
self._settings_dirty = True
def close(self) -> None:
"""Cancel work, stop the timer, clear both mailboxes. Idempotent."""
if self._closed:
return
self._closed = True
self._cancel_inflight()
self._incoming.clear()
self._composed.clear()
self._set_state("disabled")
# ------------------------------------------------------------------ internal
def _identity(self, request_id: int) -> FrameIdentity:
return FrameIdentity(
source_id=self._source_id,
session_id=self._session_id or 0,
frame_id=0,
t_rel_s=0.0,
generation=self._generation,
request_id=int(request_id),
)
def _identity_is_current(self, identity: FrameIdentity) -> bool:
return (
identity.source_id == self._source_id
and identity.generation == self._generation
and (self._session_id is None or identity.session_id == self._session_id)
)
def _start_compose(self, frame: SurfaceFieldFrame) -> None:
assets = self._assets
settings = self._settings
if assets is None:
return
generation = self._generation
# Kept so a display-settings change can recompose *this* frame instead of
# jumping to a newer one.
self._last_field_frame = frame
def work() -> None:
try:
composed = self._composer.compose(assets, frame, settings)
except Exception as exc: # noqa: BLE001 - a failed compose is not a crash
self._last_diagnostic = getattr(exc, "diagnostic", None) or diagnostic(
"field_unavailable", f"compose failed: {type(exc).__name__}: {exc}"
)
return
if composed.identity.generation != generation:
# The scene changed while this was running: the result is for a
# scene that no longer exists.
self._stale_frames += 1
return
self._composed.offer(composed)
self._inflight = self._executor.submit(work)
def _collect_inflight(self) -> None:
future = self._inflight
if future is None:
return
if not future.done():
return
self._inflight = None
try:
future.result()
except Exception as exc: # noqa: BLE001
self._last_diagnostic = getattr(exc, "diagnostic", None) or diagnostic(
"field_unavailable", f"compose raised: {type(exc).__name__}: {exc}"
)
def _cancel_inflight(self) -> None:
future = self._inflight
self._inflight = None
if future is not None:
future.cancel()
@@ -310,24 +310,12 @@ def rest_surface(assets: SceneAssets, identity: Any) -> RenderFrame:
Used to *clear* a measurement — after a refused frame, on connect, or on a
disconnect — so the screen cannot keep showing the previous acquisition's
colours as if they were current. ``quality="display_only"`` and an
``unavailable`` quantity are what tell the UI to hide the measurement bar.
colours as if they were current. The composer owns the definition; this is the
GL layer reaching it without importing the whole composition path.
"""
from orisys_app_shared.scene3d.contracts import rest_descriptor
from orisys_app_shared.scene3d.composer import build_rest_frame
part = assets.sensing_part()
count = int(part.positions_sensor_m.shape[0])
return RenderFrame(
identity=identity,
asset_hash=assets.asset_hash,
geometry_id=assets.geometry_id,
sensing_part_id=part.part_id,
positions_sensor_m=part.positions_sensor_m,
normals_sensor=part.normals_sensor,
scalar=freeze(np.full(count, np.nan, dtype=np.float32), dtype=np.float32),
valid=freeze(np.zeros(count, dtype=bool), dtype=np.bool_),
scalar_descriptor=rest_descriptor(),
)
return build_rest_frame(assets, identity)
# ------------------------------------------------------------------- dispatch