From 19ae1f4968dc09b379817b07e12ba6148f1e0808 Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Thu, 8 Oct 2026 16:10:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=89=E7=BB=B4=E6=98=BE=E7=A4=BA=E4=B8=8E?= =?UTF-8?q?=E4=BA=8C=E7=BB=B4=E5=85=B1=E7=94=A8=E6=B7=B1=E5=BA=A6=E8=89=B2?= =?UTF-8?q?=E6=A0=87=EF=BC=8C=E9=85=8D=E7=BD=AE=E5=90=8D=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=20Finger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 箭头阈值写入设置;去掉网格告警。三维色标改为立刻采用二维量程(不再 P5/P95 缓变)。选择器显示 Finger。已用 pane UI、shared-scale 与 catalog 单测验证。 --- .../config/sensors/finger/manifest.json | 2 +- apps/studio/docs/sensor-bundles.md | 2 +- .../src/orisys_studio/scene3d/gl_backend.py | 22 +++++++++ .../studio/src/orisys_studio/scene3d/panel.py | 48 ++++++++++++++----- apps/studio/src/orisys_studio/scene3d/view.py | 2 + apps/studio/tests/test_scene3d_pane_ui.py | 19 +++++++- apps/studio/tests/test_scene3d_view.py | 2 +- apps/studio/tests/test_sensor_catalog_root.py | 4 +- apps/studio/tests/test_shared_depth_scale.py | 6 +-- 9 files changed, 85 insertions(+), 22 deletions(-) diff --git a/apps/studio/config/sensors/finger/manifest.json b/apps/studio/config/sensors/finger/manifest.json index 6f87d2f..59a6072 100644 --- a/apps/studio/config/sensors/finger/manifest.json +++ b/apps/studio/config/sensors/finger/manifest.json @@ -1,7 +1,7 @@ { "schema_version": 1, "profile_id": "orisys.finger.r01", - "display_name": "Finger R01", + "display_name": "Finger", "product_family": "finger", "processing_backend": "uv_unroll", "sensor_json": "sensor.json", diff --git a/apps/studio/docs/sensor-bundles.md b/apps/studio/docs/sensor-bundles.md index 299e8af..8ac0ceb 100644 --- a/apps/studio/docs/sensor-bundles.md +++ b/apps/studio/docs/sensor-bundles.md @@ -31,7 +31,7 @@ same rows. |---|---|---| | `schema_version` | `1` | The SDK's current descriptor schema (`CURRENT_DESCRIPTOR_SCHEMA`). | | `profile_id` | `orisys.finger.r01` | The processing profile this folder is: finger design revision `r01`, whose unroll map and camera scale the configuration pins. Never a product-family name. | -| `display_name` | `Finger R01` | Shown in the picker. Excluded from the processing fingerprint. | +| `display_name` | `Finger` | Shown in the picker. Excluded from the processing fingerprint. | | `product_family` | `finger` | What the operator calls it. `finger-mems` is a different family on a different pipeline. | | `processing_backend` | `uv_unroll` | The pipeline the finger actually runs: `FingerRuntime` builds a UV domain from the export's unroll map, then flow and force. | | `sensor_json` | `sensor.json` | The configuration, still the only place camera, tracker and contact settings live. | diff --git a/apps/studio/src/orisys_studio/scene3d/gl_backend.py b/apps/studio/src/orisys_studio/scene3d/gl_backend.py index cbc7890..aeb0868 100644 --- a/apps/studio/src/orisys_studio/scene3d/gl_backend.py +++ b/apps/studio/src/orisys_studio/scene3d/gl_backend.py @@ -263,6 +263,24 @@ def shader_source(profile: Profile, program: str) -> tuple[str, str]: return header + vertex, header + body +def _finite_minmax(scalar, valid) -> tuple[float, float] | None: + """Finite minimum and maximum — the same interval ``FrameView.frame_depth_span`` uses.""" + import numpy as np + + values = np.asarray(scalar, dtype=np.float64).reshape(-1) + mask = np.asarray(valid, dtype=bool).reshape(-1) + if values.size != mask.size: + return None + usable = values[mask & np.isfinite(values)] + if usable.size == 0: + return None + lo = float(np.min(usable)) + hi = float(np.max(usable)) + if hi <= lo: + hi = lo + 1.0 + return lo, hi + + def _valid_scalar_range( scalar, valid, *, signed: bool = False ) -> tuple[float, float] | None: @@ -379,6 +397,10 @@ def _field_scalar_range( hi = float(shared_depth_scale[1]) if hi > lo and not (math.isnan(lo) or math.isnan(hi)): return lo, hi + if quantity == "depth_response": + # Same interval as the 2D canvas (finite min/max). A P5/P95 blend here + # made the 3D colour bar crawl toward a different number than the 2D label. + return _finite_minmax(field.scalar, field.valid) measured = _valid_scalar_range( field.scalar, field.valid, diff --git a/apps/studio/src/orisys_studio/scene3d/panel.py b/apps/studio/src/orisys_studio/scene3d/panel.py index 8ee806a..dde3c95 100644 --- a/apps/studio/src/orisys_studio/scene3d/panel.py +++ b/apps/studio/src/orisys_studio/scene3d/panel.py @@ -159,6 +159,9 @@ class ThreeDPanel(QFrame): #: prefs (or the dataclass defaults) so a colour-scale click cannot reset them. self._flow_grid_spacing = 10 self._max_arrows = 400 + #: Depth colour span published by the 2D canvas. Shown here immediately so the + #: two legends do not wait on the next GL paint (which is rate-limited). + self._shared_depth_span: tuple[float, float] | None = None root = QVBoxLayout(self) root.setContentsMargins(8, 6, 8, 6) @@ -490,7 +493,16 @@ class ThreeDPanel(QFrame): if quantity.startswith("scene3d."): quantity = descriptor.quantity quality = t(f"scene3d.quality_{descriptor.quality}") - if effective is None or effective.scale_mode == "": + shared = ( + self._shared_depth_span + if str(descriptor.quantity) == "depth_response" + else None + ) + if shared is not None: + lo, hi = shared + elif effective is not None and effective.scale_mode: + lo, hi = effective.scale_lower, effective.scale_upper + else: self._set_legend( t( "scene3d.legend", @@ -503,7 +515,7 @@ class ThreeDPanel(QFrame): return mode = t( "scene3d.legend_auto" - if effective.scale_mode == "automatic" + if (effective is None or effective.scale_mode != "fixed") else "scene3d.legend_fixed" ) self._set_legend( @@ -512,15 +524,25 @@ class ThreeDPanel(QFrame): quantity=quantity, unit=descriptor.unit, mode=mode, - # Three significant digits: the footer is one line, and "0.033 – 0.90" - # says as much as "0.03337 – 0.8993" while leaving room for the rest. - lower=f"{effective.scale_lower:.3g}", - upper=f"{effective.scale_upper:.3g}", + lower=self._format_bound(lo), + upper=self._format_bound(hi), quality=quality, ) ) self.legend_label.setVisible(self._show_diagnostics) + def set_shared_depth_scale(self, span: tuple[float, float] | None) -> None: + """Adopt the 2D canvas's depth colour span and refresh the legend now.""" + if span is None: + self._shared_depth_span = None + else: + self._shared_depth_span = (float(span[0]), float(span[1])) + self._render_scale_bounds() + self._render_legend() + + def _format_bound(self, value: float) -> str: + return f"{float(value):.3f}" + def _render_scale_bounds(self) -> None: """Show only the numeric bounds needed to read the color ramp.""" descriptor = self._descriptor @@ -531,16 +553,20 @@ class ThreeDPanel(QFrame): upper = "" mode = str(self.scale_combo.currentData() or "fixed") effective = self._effective_scale - if visible and mode == "fixed": - lower = f"{self.lower_spin.value():.3g}" - upper = f"{self.upper_spin.value():.3g}" + quantity = "" if descriptor is None else str(descriptor.quantity) + if visible and quantity == "depth_response" and self._shared_depth_span is not None: + lower = self._format_bound(self._shared_depth_span[0]) + upper = self._format_bound(self._shared_depth_span[1]) + elif visible and mode == "fixed": + lower = self._format_bound(self.lower_spin.value()) + upper = self._format_bound(self.upper_spin.value()) elif ( visible and effective is not None and effective.scale_mode == "automatic" ): - lower = f"{effective.scale_lower:.3g}" - upper = f"{effective.scale_upper:.3g}" + lower = self._format_bound(effective.scale_lower) + upper = self._format_bound(effective.scale_upper) self.scale_lower_label.setText(lower) self.scale_upper_label.setText(upper) self.scale_lower_label.setVisible(visible) diff --git a/apps/studio/src/orisys_studio/scene3d/view.py b/apps/studio/src/orisys_studio/scene3d/view.py index 617c6ee..07f67af 100644 --- a/apps/studio/src/orisys_studio/scene3d/view.py +++ b/apps/studio/src/orisys_studio/scene3d/view.py @@ -317,6 +317,8 @@ class Scene3DView(QWidget): """Colour depth with the 2D canvas's latched ``(lo, hi)``.""" if self._widget is not None: self._widget.set_shared_depth_scale(span) + if self._panel is not None: + self._panel.set_shared_depth_scale(span) def set_theme(self, theme: str) -> None: """Adopt the application theme's colour ramp. diff --git a/apps/studio/tests/test_scene3d_pane_ui.py b/apps/studio/tests/test_scene3d_pane_ui.py index 10eb80a..02b33ff 100644 --- a/apps/studio/tests/test_scene3d_pane_ui.py +++ b/apps/studio/tests/test_scene3d_pane_ui.py @@ -129,6 +129,21 @@ def test_arrow_threshold_round_trips_and_feeds_the_shared_policy(panel): assert policy_for_settings(again).flow_on_px == pytest.approx(0.9) +def test_depth_legend_follows_the_shared_2d_span_immediately(panel): + """The 3D colour-bar numbers are the 2D span, not a lagged GL automatic range.""" + from orisys_app_shared.scene3d.contracts import FieldDescriptor, RenderStats + + panel.set_descriptor( + FieldDescriptor("depth_response", "1", "approximate", False) + ) + panel.set_render_stats( + RenderStats(scale_lower=0.0, scale_upper=0.0489, scale_mode="automatic") + ) + panel.set_shared_depth_scale((0.0, 0.029)) + assert panel.scale_lower_label.text() == "0.000" + assert panel.scale_upper_label.text() == "0.029" + + def test_source_kind_analysis_uses_its_own_label(panel): """A stored result is not labelled as live acquisition.""" panel.set_diagnostics_visible(True) @@ -241,8 +256,8 @@ def test_the_diagnostics_switch_governs_what_the_pane_explains(panel): ) panel.set_diagnostics_visible(False) assert not panel.scale_bar.isHidden() - assert panel.scale_lower_label.text() == "0.25" - assert panel.scale_upper_label.text() == "2.5" + assert panel.scale_lower_label.text() == "0.250" + assert panel.scale_upper_label.text() == "2.500" assert not panel.scale_lower_label.isHidden() assert not panel.scale_upper_label.isHidden() assert panel.legend_label.isHidden() diff --git a/apps/studio/tests/test_scene3d_view.py b/apps/studio/tests/test_scene3d_view.py index eb47bcf..0c4ffe3 100644 --- a/apps/studio/tests/test_scene3d_view.py +++ b/apps/studio/tests/test_scene3d_view.py @@ -1018,7 +1018,7 @@ def test_the_legend_shows_the_range_the_shader_actually_used(qt_app): ) ) legend = panel.legend_label.text() - assert "0.25" in legend and "7.5" in legend, legend + assert "0.250" in legend and "7.500" in legend, legend assert t("scene3d.legend_auto") in legend, legend assert panel.scale_bar.isVisible() or panel.scale_bar.parent() is not None assert "flow_magnitude" not in legend diff --git a/apps/studio/tests/test_sensor_catalog_root.py b/apps/studio/tests/test_sensor_catalog_root.py index b606a79..b11d81c 100644 --- a/apps/studio/tests/test_sensor_catalog_root.py +++ b/apps/studio/tests/test_sensor_catalog_root.py @@ -51,13 +51,13 @@ EXPECTED_SHIPPED: dict[str, tuple[str, str, str, bool, str]] = { } #: What an operator reads in the picker, one row per shipped bundle: the bundle's name, and -#: nothing else. ``finger`` declares ``Finger R01`` in its descriptor; the other three have no +#: nothing else. ``finger`` declares ``Finger`` in its descriptor; the other three have no #: descriptor and are named by the folder they live in, as the SDK's built-in table words it. #: The origin, the calibration selection and the SDK's ``degraded`` verdict are not rows — they #: are read in the dialog's details surface, which sits under the selector. EXPECTED_ROWS: dict[str, str] = { "box": "Box", - "finger": "Finger R01", + "finger": "Finger", "finger-mems": "Finger (MEMS)", "mini": "Mini", } diff --git a/apps/studio/tests/test_shared_depth_scale.py b/apps/studio/tests/test_shared_depth_scale.py index e9b2152..6e342ea 100644 --- a/apps/studio/tests/test_shared_depth_scale.py +++ b/apps/studio/tests/test_shared_depth_scale.py @@ -43,12 +43,10 @@ def test_depth_without_a_shared_span_still_measures_the_field() -> None: field = _field("depth_response", np.linspace(0.0, 9.0, 16).reshape(4, 4)) span = _field_scalar_range( field, - previous=None, + previous=(0.0, 2.0), shared_depth_scale=None, ) - assert span is not None - assert span[0] == 0.0 - assert span[1] > 1.0 + assert span == (0.0, 9.0) def test_a_degenerate_shared_span_falls_back_to_the_measurement() -> None: