统一力值阈值处理并保留原始采样
移除自动显示基线,使逐轴阈值统一作用于曲线、读数与记录输出,标定拟合继续使用原始采样。 共享层全量测试和相关 Studio 测试通过。
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.force.presentation import threshold_force
|
||||
from orisys_app_shared.force.calibration_io import (
|
||||
ChannelKey,
|
||||
)
|
||||
@@ -77,6 +78,8 @@ class CalPanel(QFrame):
|
||||
self._live_fn = 0.0
|
||||
self._live_fx = 0.0
|
||||
self._live_fy = 0.0
|
||||
self._raw_threshold = 50.0
|
||||
self._raw_decimals = 0
|
||||
self._sensor_type = "box"
|
||||
self._config_ref: str | None = None
|
||||
self._mono = _mono_font()
|
||||
@@ -305,6 +308,12 @@ class CalPanel(QFrame):
|
||||
self._live_fy = float(fy)
|
||||
self._render_live_label()
|
||||
|
||||
def set_output_settings(self, *, raw_threshold: float, raw_decimals: int) -> None:
|
||||
self._raw_threshold = max(0.0, float(raw_threshold))
|
||||
self._raw_decimals = max(0, int(raw_decimals))
|
||||
self._render_live_label()
|
||||
self._refresh_table()
|
||||
|
||||
def set_progress(self, fraction: float, elapsed_s: float, total_s: float) -> None:
|
||||
self.progress.setVisible(True)
|
||||
self.progress.setValue(max(0, min(1000, int(fraction * 1000))))
|
||||
@@ -435,9 +444,10 @@ class CalPanel(QFrame):
|
||||
("fx", self._live_fx),
|
||||
("fy", self._live_fy),
|
||||
):
|
||||
rendered = f"{value:.{self._raw_decimals}f}" if np.isfinite(value) else "—"
|
||||
parts.append(
|
||||
f"<span style='color:{colors[key]}; font-weight:600'>{key.upper()}</span>"
|
||||
f" {value:.3f}"
|
||||
f" {rendered}"
|
||||
)
|
||||
self.live_label.setText(
|
||||
f"<span style='color:{color(self._theme, 'text_secondary')}'>"
|
||||
@@ -529,15 +539,21 @@ class CalPanel(QFrame):
|
||||
self.empty_hint.setVisible(not points)
|
||||
self._refresh_fit_label()
|
||||
|
||||
@staticmethod
|
||||
def _axis_cell(point, channel: str) -> str:
|
||||
value = point.axis_mean(channel)
|
||||
def _axis_cell(self, point, channel: str) -> str:
|
||||
column = {"fn": 2, "fx": 3, "fy": 4}[channel]
|
||||
values = [
|
||||
threshold_force(sample[column], self._raw_threshold)
|
||||
for sample in point.samples if bool(sample[8]) and np.isfinite(sample[column])
|
||||
]
|
||||
value = float(np.mean(values)) if values else threshold_force(
|
||||
point.axis_mean(channel), self._raw_threshold
|
||||
)
|
||||
if not np.isfinite(value):
|
||||
return "—"
|
||||
if channel == point.channel and np.isfinite(point.residual_n):
|
||||
# Residual of the fit on the same cell as the value it came from.
|
||||
return f"{value:.4f} ({point.residual_n:+.4f})"
|
||||
return f"{value:.4f}"
|
||||
return f"{value:.{self._raw_decimals}f} ({point.residual_n:+.4f})"
|
||||
return f"{value:.{self._raw_decimals}f}"
|
||||
|
||||
@staticmethod
|
||||
def _stability_cell(point) -> str:
|
||||
@@ -598,7 +614,10 @@ class CalPanel(QFrame):
|
||||
restore_diagnostics(point)
|
||||
if not point.trace:
|
||||
return None
|
||||
return CalibrationPointDialog(point, theme=self._theme, parent=self)
|
||||
return CalibrationPointDialog(
|
||||
point, theme=self._theme, parent=self,
|
||||
raw_threshold=self._raw_threshold, raw_decimals=self._raw_decimals,
|
||||
)
|
||||
|
||||
def _on_row_activated(self) -> None:
|
||||
"""Double click a row to inspect that point's three-axis curve."""
|
||||
|
||||
@@ -7,6 +7,7 @@ the three-panel layout, while a single point can still be examined in full.
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.force.presentation import threshold_force, threshold_values
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtGui import QFont, QFontDatabase
|
||||
@@ -40,10 +41,15 @@ def _mono_font(point_size: int = 9) -> QFont:
|
||||
class CalibrationPointDialog(QDialog):
|
||||
"""One recorded point: all three raw traces plus its fit residual."""
|
||||
|
||||
def __init__(self, point, *, theme: str = "dark", parent=None) -> None:
|
||||
def __init__(
|
||||
self, point, *, theme: str = "dark", parent=None,
|
||||
raw_threshold: float = 0.0, raw_decimals: int = 0,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self._point = point
|
||||
self._theme = theme
|
||||
self._raw_threshold = max(0.0, float(raw_threshold))
|
||||
self._raw_decimals = max(0, int(raw_decimals))
|
||||
self.setWindowTitle(
|
||||
t(
|
||||
"cal.point_title",
|
||||
@@ -82,7 +88,15 @@ class CalibrationPointDialog(QDialog):
|
||||
if axis is None or not np.isfinite(axis.mean):
|
||||
stats.append(f"{key.upper()} —")
|
||||
continue
|
||||
stats.append(f"{key.upper()} {axis.mean:.4f} ± {axis.std:.4f}")
|
||||
values = np.asarray(point.trace.get(key, ()), dtype=float)
|
||||
if values.size:
|
||||
values, _ = threshold_values(values, self._raw_threshold)
|
||||
stats.append(
|
||||
f"{key.upper()} {np.nanmean(values):.{self._raw_decimals}f}"
|
||||
f" ± {np.nanstd(values):.{self._raw_decimals}f}"
|
||||
)
|
||||
else:
|
||||
stats.append(f"{key.upper()} {threshold_force(axis.mean, self._raw_threshold):.{self._raw_decimals}f}")
|
||||
value = QLabel(" ".join(stats))
|
||||
value.setFont(_mono_font())
|
||||
self._add_row(form, "cal.point_axes", "")
|
||||
@@ -137,8 +151,10 @@ class CalibrationPointDialog(QDialog):
|
||||
continue
|
||||
step = self._point.trace_dt_s or 0.01
|
||||
times = np.arange(len(values), dtype=float) * step
|
||||
values = np.asarray(values, dtype=float)
|
||||
values, _ = threshold_values(values, self._raw_threshold)
|
||||
plot.plot(
|
||||
times,
|
||||
np.asarray(values, dtype=float),
|
||||
values,
|
||||
pen=pg.mkPen(color(self._theme, TRACE_TOKENS[row]), width=2),
|
||||
)
|
||||
|
||||
@@ -10,8 +10,6 @@ from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.force import MetricHistory
|
||||
from orisys_app_shared.force.history import DEFAULT_MAX_FPS
|
||||
from orisys_app_shared.force.presentation import (
|
||||
DEFAULT_MIN_DEADBAND_N,
|
||||
DEFAULT_MIN_DEADBAND_RAW,
|
||||
ForcePresentation,
|
||||
)
|
||||
from orisys_app_shared.force.source import StoreCurveSource
|
||||
@@ -108,8 +106,8 @@ def format_force(
|
||||
if presentation is not None:
|
||||
return presentation.format_force(value, unit=unit)
|
||||
if unit == "N":
|
||||
return f"{value:.3f}"
|
||||
return f"{value:.1f}"
|
||||
return f"{value:.2f}"
|
||||
return f"{value:.0f}"
|
||||
|
||||
|
||||
def unit_display(unit: str) -> str:
|
||||
@@ -176,13 +174,10 @@ class ForcePlotsPanel(QFrame):
|
||||
self._last_values: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
||||
#: Per axis: does ``_last_values`` hold a real value in the active unit?
|
||||
self._available: tuple[bool, bool, bool] = (True, True, True)
|
||||
#: Display-only baseline / deadband for the value cards *and* the curves.
|
||||
self.presentation = ForcePresentation(enabled=False)
|
||||
#: The operator's master switch for that baseline. It is owned by
|
||||
#: ``display_prefs`` (the ``raw_auto_zero`` preference) and kept here so a
|
||||
#: caller that does not know it cannot switch the aid off (see
|
||||
#: ``configure_presentation``).
|
||||
self._presentation_enabled = False
|
||||
#: Shared absolute threshold and numeric precision for values and curves.
|
||||
self.presentation = ForcePresentation(enabled=True)
|
||||
#: Kept for callers that explicitly configure presentation state.
|
||||
self._presentation_enabled = True
|
||||
self._sensor_type = "box"
|
||||
self._display_values: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
||||
self._display_suppressed: tuple[bool, bool, bool] = (False, False, False)
|
||||
@@ -193,6 +188,7 @@ class ForcePlotsPanel(QFrame):
|
||||
#: True once a source is attached (either kind). ``_store is None`` was
|
||||
#: the old proxy for this and could not express a disk source.
|
||||
self._source_ready = False
|
||||
self._source_values_processed = False
|
||||
#: Non-empty when there is no trace to draw *and that is a fact about the input*:
|
||||
#: a bare video whose pass has not run, or a profile whose chain produced no force
|
||||
#: for it. The mode line states it rather than leaving an empty plot under wording
|
||||
@@ -557,31 +553,24 @@ class ForcePlotsPanel(QFrame):
|
||||
self._apply_axis_mode()
|
||||
|
||||
def set_deadband_floors(self, *, newton: float | None = None, raw: float | None = None) -> None:
|
||||
"""The operator's display deadband floors; 0 means "auto (estimated)".
|
||||
|
||||
A floor, not a replacement: the estimate from the quiet window still
|
||||
applies underneath, so this is how an operator widens the zero zone (or
|
||||
pins one when the calibration data is suspect) without re-fitting.
|
||||
"""
|
||||
"""Set absolute per-unit output thresholds; zero disables suppression."""
|
||||
if newton is not None:
|
||||
value = max(0.0, float(newton))
|
||||
self.presentation.set_min_deadband(
|
||||
"N", value if value > 0.0 else DEFAULT_MIN_DEADBAND_N
|
||||
)
|
||||
self.presentation.set_min_deadband("N", value)
|
||||
if raw is not None:
|
||||
value = max(0.0, float(raw))
|
||||
self.presentation.set_min_deadband(
|
||||
"raw", value if value > 0.0 else DEFAULT_MIN_DEADBAND_RAW
|
||||
)
|
||||
self.presentation.set_min_deadband("raw", value)
|
||||
self._render_value_cards()
|
||||
|
||||
def set_value_decimals(self, *, newton: int, raw: int) -> None:
|
||||
self.presentation.newton_decimals = max(0, int(newton))
|
||||
self.presentation.raw_decimals = max(0, int(raw))
|
||||
self._render_value_cards()
|
||||
|
||||
def set_presentation_enabled(self, enabled: bool) -> None:
|
||||
"""The operator's master switch for the display baseline and deadband."""
|
||||
"""Enable or disable thresholding for a standalone chart source."""
|
||||
self._presentation_enabled = bool(enabled)
|
||||
self.presentation.set_enabled(self._presentation_enabled, unit=self._unit)
|
||||
# Switching the aid off must show the measurement again, and switching it
|
||||
# on must show the policy — from the same values the curve is drawn with,
|
||||
# or a card and its own curve would disagree for a frame.
|
||||
self._refresh_display_values()
|
||||
self._render_value_cards()
|
||||
|
||||
@@ -590,13 +579,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self._apply_axis_mode()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Back to the view defaults: empty curves, follow mode, unit ``raw``.
|
||||
|
||||
The display baseline is deliberately *kept*: a view reset is not a
|
||||
statement about the device, and the operator's zero estimate (or its
|
||||
absence) survives it. A new session restarts the estimate through
|
||||
``configure_presentation``.
|
||||
"""
|
||||
"""Restore view defaults while retaining the configured threshold."""
|
||||
self.history.reset()
|
||||
self._clear_curves()
|
||||
self._last_values = (0.0, 0.0, 0.0)
|
||||
@@ -626,57 +609,13 @@ class ForcePlotsPanel(QFrame):
|
||||
unit: str,
|
||||
enabled: bool | None = None,
|
||||
) -> None:
|
||||
"""Point the display baseline at the sensor and unit now being shown.
|
||||
|
||||
The aid is for every sensor type and both units — an idle channel that
|
||||
reads a raw count or an offset in newtons is a display problem whichever
|
||||
device produced it — so the sensor type only names what is drawn.
|
||||
|
||||
``enabled`` sets the master switch directly when given; the operator's
|
||||
``raw_auto_zero`` preference is what the run-start call site passes.
|
||||
Omitting it leaves the current switch alone.
|
||||
"""
|
||||
"""Configure the chart's unit and optional threshold switch."""
|
||||
self._sensor_type = str(sensor_type or "box")
|
||||
if enabled is not None:
|
||||
self._presentation_enabled = bool(enabled)
|
||||
self.presentation.enabled = bool(self._presentation_enabled)
|
||||
self.presentation.reset_session(unit=unit)
|
||||
|
||||
def request_presentation_reestimate(self) -> None:
|
||||
"""Restart the idle baseline window (display only: cards and curves)."""
|
||||
self.presentation.request_reestimate()
|
||||
# A restarted window starts from nothing, so the cards go back to the
|
||||
# measurement for the couple of samples it takes to re-learn the offset
|
||||
# — derived by the same call the curve uses, never left stale.
|
||||
self._refresh_display_values()
|
||||
self._render_value_cards()
|
||||
|
||||
def observe_for_baseline(
|
||||
self,
|
||||
values: tuple[float, float, float],
|
||||
*,
|
||||
t_rel_s: float,
|
||||
valid: bool = True,
|
||||
flow_p95: float | None = None,
|
||||
contact_found: bool = False,
|
||||
unit: str | None = None,
|
||||
) -> None:
|
||||
"""Feed the startup estimator in the unit these values are expressed in.
|
||||
|
||||
The curve and the record keep the raw measurement; the estimate is only
|
||||
ever a display copy.
|
||||
"""
|
||||
if not self.presentation.enabled:
|
||||
return
|
||||
self.presentation.observe(
|
||||
values,
|
||||
t_rel_s=t_rel_s,
|
||||
valid=valid,
|
||||
flow_p95=flow_p95,
|
||||
contact_found=contact_found,
|
||||
unit=unit,
|
||||
)
|
||||
|
||||
def push(
|
||||
self,
|
||||
t_rel_s: float,
|
||||
@@ -712,16 +651,6 @@ class ForcePlotsPanel(QFrame):
|
||||
self._last_values = (float(fn), float(fx), float(fy))
|
||||
if available is not None:
|
||||
self._available = tuple(bool(flag) for flag in available)
|
||||
self.observe_for_baseline(
|
||||
self._last_values,
|
||||
t_rel_s=float(t_rel_s),
|
||||
valid=bool(forces_valid),
|
||||
flow_p95=flow_p95,
|
||||
contact_found=contact_found,
|
||||
# The values are in ``unit``, and the estimate is kept per unit, so
|
||||
# the sample must be filed under the unit it was measured in.
|
||||
unit=unit,
|
||||
)
|
||||
displayed = self.presentation.accept(
|
||||
self._last_values, unit=unit, available=self._available
|
||||
)
|
||||
@@ -816,8 +745,6 @@ class ForcePlotsPanel(QFrame):
|
||||
if unit == self._unit:
|
||||
return
|
||||
self._unit = unit
|
||||
# The display policy keeps one estimate per unit, so it has to know which
|
||||
# one is on screen: a stale "raw" is ready does not mean newtons are.
|
||||
self.presentation.unit = unit
|
||||
# ``clear`` keeps the buffer's time origin on purpose: the rebuilt history
|
||||
# is in the same session-relative base, so the X axis must not restart.
|
||||
@@ -852,6 +779,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self._live_store = store
|
||||
self._source = StoreCurveSource(store)
|
||||
self._source_ready = store is not None
|
||||
self._source_values_processed = True
|
||||
self._source_absent_reason = ""
|
||||
self._on_source_changed()
|
||||
|
||||
@@ -861,6 +789,13 @@ class ForcePlotsPanel(QFrame):
|
||||
|
||||
self._source = RecordingCurveSource(reader)
|
||||
self._source_ready = True
|
||||
columns = getattr(reader.index(), "_columns", {})
|
||||
self._source_values_processed = all(
|
||||
name in columns for name in (
|
||||
"FN_output_raw", "FX_output_raw", "FY_output_raw",
|
||||
"FN_output_N", "FX_output_N", "FY_output_N",
|
||||
)
|
||||
)
|
||||
self._source_absent_reason = ""
|
||||
self._on_source_changed()
|
||||
|
||||
@@ -876,6 +811,7 @@ class ForcePlotsPanel(QFrame):
|
||||
``source is None`` with a ``reason`` is a state, not an empty chart: the mode line
|
||||
states that reason instead of leaving "following live" over a flat plot.
|
||||
"""
|
||||
self._source_values_processed = False
|
||||
if source is None:
|
||||
self._source = StoreCurveSource(self._live_store)
|
||||
self._source_ready = False
|
||||
@@ -1149,8 +1085,10 @@ class ForcePlotsPanel(QFrame):
|
||||
# The cards describe this point, so they carry the display copy of it —
|
||||
# including while browsing or replaying, where the values did not come
|
||||
# from the live sample the last ``push`` presented.
|
||||
presented, suppressed = self.presentation.present(
|
||||
np.asarray([values], dtype=float), unit=self._unit
|
||||
presented, suppressed = (
|
||||
(np.asarray([values], dtype=float), np.zeros((1, 3), dtype=bool))
|
||||
if self._source_values_processed
|
||||
else self.presentation.present(np.asarray([values], dtype=float), unit=self._unit)
|
||||
)
|
||||
self._display_values = tuple(float(value) for value in presented[0])
|
||||
self._display_suppressed = tuple(bool(flag) for flag in suppressed[0])
|
||||
@@ -1248,9 +1186,7 @@ class ForcePlotsPanel(QFrame):
|
||||
buckets=self._overview_points(), unit=self._unit
|
||||
)
|
||||
values = np.asarray(window.values, dtype=float)
|
||||
# The envelope is drawn from the same store measurement as the window
|
||||
# above, so it goes through the same presentation: the overview thumb
|
||||
# must not show a jittering baseline the main chart has flattened.
|
||||
# The overview uses the same processed values as the main curves.
|
||||
values = self._curve_values(values)
|
||||
column = values[:, 0]
|
||||
finite = np.isfinite(column)
|
||||
@@ -1397,7 +1333,7 @@ class ForcePlotsPanel(QFrame):
|
||||
|
||||
Raw mode used to auto-range, which collapsed the axis onto the idle
|
||||
jitter and then rescaled on every contact. A fixed raw range keeps the
|
||||
baseline visually flat: the raw maxima are sized so the idle jitter is
|
||||
near-zero region visually flat: the raw maxima are sized so idle jitter is
|
||||
well under one pixel.
|
||||
"""
|
||||
if self._unit == "N":
|
||||
@@ -1460,29 +1396,22 @@ class ForcePlotsPanel(QFrame):
|
||||
zero_line.setVisible(bool(ymin <= 0.0 <= ymax))
|
||||
|
||||
def _curve_values(self, values) -> np.ndarray:
|
||||
"""The values the curves draw: the measurement through the display policy.
|
||||
|
||||
``history``, the session store and ``forces.csv`` keep the measurement;
|
||||
the baseline and deadband are applied here, on the way to the plot only.
|
||||
That is what makes a resting channel a flat line at exactly zero without
|
||||
the record being edited. Browsing and a replay read the store, so they go
|
||||
through this same presentation — the curves show the operator's display
|
||||
policy, the store shows what was measured.
|
||||
"""
|
||||
displayed, _suppressed = self.presentation.present(values, unit=self._unit)
|
||||
"""Use stored output values, or threshold a legacy unprocessed source."""
|
||||
displayed, _suppressed = (
|
||||
(np.asarray(values, dtype=float), None)
|
||||
if self._source_values_processed
|
||||
else self.presentation.present(values, unit=self._unit)
|
||||
)
|
||||
return displayed
|
||||
|
||||
def _refresh_display_values(self) -> None:
|
||||
"""Re-derive the card values from the policy the curves are drawn with.
|
||||
|
||||
``push`` fills these from :meth:`ForcePresentation.accept`, whose
|
||||
hysteresis is what keeps a *live* reading steady. Every other path — a
|
||||
re-estimate, the master switch, a unit switch — goes through the same
|
||||
stateless :meth:`ForcePresentation.present` the curve uses, so a card can
|
||||
never state a value its own curve does not carry.
|
||||
"""
|
||||
displayed, suppressed = self.presentation.present(
|
||||
np.asarray([self._last_values], dtype=float), unit=self._unit
|
||||
"""Refresh cards from the same values that the curves draw."""
|
||||
displayed, suppressed = (
|
||||
(np.asarray([self._last_values], dtype=float), np.zeros((1, 3), dtype=bool))
|
||||
if self._source_values_processed
|
||||
else self.presentation.present(
|
||||
np.asarray([self._last_values], dtype=float), unit=self._unit
|
||||
)
|
||||
)
|
||||
self._display_values = tuple(float(value) for value in displayed[0])
|
||||
self._display_suppressed = tuple(bool(flag) for flag in suppressed[0])
|
||||
@@ -1499,7 +1428,11 @@ class ForcePlotsPanel(QFrame):
|
||||
a single long line under the charts, which could not fit and pushed the
|
||||
panel wider.
|
||||
"""
|
||||
fmt = "{value:.3f}" if self._unit == "N" else "{value:.1f}"
|
||||
decimals = (
|
||||
self.presentation.newton_decimals
|
||||
if self._unit == "N" else self.presentation.raw_decimals
|
||||
)
|
||||
fmt = "{value:." + str(decimals) + "f}"
|
||||
unavailable = t("chart.value_unavailable")
|
||||
selecting = self._selection
|
||||
for index, (value_label, unit_label, detail_label, value) in enumerate(
|
||||
@@ -1516,12 +1449,6 @@ class ForcePlotsPanel(QFrame):
|
||||
available = bool(np.isfinite(peak))
|
||||
value = peak
|
||||
elif self.presentation.enabled:
|
||||
# The policy is *live from the first samples*: there is no
|
||||
# separate "the estimate is ready" gate here. Such a gate is what
|
||||
# let a card read FN 1118 raw while its own curve was already flat
|
||||
# on zero, because the card fell back to the raw measurement and
|
||||
# the curve did not. Both are ``_display_values`` / ``present()``
|
||||
# of the same applied estimate.
|
||||
value = self._display_values[index]
|
||||
if not available:
|
||||
# An axis with no curve in this unit: a dash, never a raw count
|
||||
|
||||
@@ -48,12 +48,11 @@ DEFAULT_PLOT_SHEAR_MAX = 20.0
|
||||
DEFAULT_PLOT_FN_RAW_MIN = 0.0
|
||||
DEFAULT_PLOT_FN_RAW_MAX = 1000000.0
|
||||
DEFAULT_PLOT_SHEAR_RAW_MAX = 500000.0
|
||||
#: Display deadband floors ("归零阈值") in the unit each applies to. 0 means
|
||||
#: "auto": the percentile the presentation layer estimates from the quiet
|
||||
#: startup window. Non-zero forces a floor under that estimate, so an operator
|
||||
#: can widen the zero zone (or pin one when calibration data is suspect).
|
||||
#: Absolute output thresholds in each unit; zero disables suppression.
|
||||
DEFAULT_PLOT_DEADBAND_N = 0.0
|
||||
DEFAULT_PLOT_DEADBAND_RAW = 0.0
|
||||
DEFAULT_PLOT_DEADBAND_RAW = 50.0
|
||||
DEFAULT_PLOT_NEWTON_DECIMALS = 2
|
||||
DEFAULT_PLOT_RAW_DECIMALS = 0
|
||||
|
||||
|
||||
class PlotSettingsDialog(QDialog):
|
||||
@@ -70,6 +69,8 @@ class PlotSettingsDialog(QDialog):
|
||||
shear_raw_max: float = DEFAULT_PLOT_SHEAR_RAW_MAX,
|
||||
deadband_n: float = DEFAULT_PLOT_DEADBAND_N,
|
||||
deadband_raw: float = DEFAULT_PLOT_DEADBAND_RAW,
|
||||
newton_decimals: int = DEFAULT_PLOT_NEWTON_DECIMALS,
|
||||
raw_decimals: int = DEFAULT_PLOT_RAW_DECIMALS,
|
||||
units: str = "newton",
|
||||
filters: FilterSettings | None = None,
|
||||
filters_locked: bool = False,
|
||||
@@ -160,6 +161,17 @@ class PlotSettingsDialog(QDialog):
|
||||
self.deadband_raw_spin.setSingleStep(5000.0)
|
||||
self.deadband_raw_spin.setDecimals(0)
|
||||
self.deadband_raw_spin.setValue(float(deadband_raw))
|
||||
self.newton_decimals_spin = QSpinBox()
|
||||
self.newton_decimals_spin.setRange(0, 6)
|
||||
self.newton_decimals_spin.setValue(int(newton_decimals))
|
||||
self.raw_decimals_spin = QSpinBox()
|
||||
self.raw_decimals_spin.setRange(0, 6)
|
||||
self.raw_decimals_spin.setValue(int(raw_decimals))
|
||||
precision_row = QHBoxLayout()
|
||||
precision_row.addWidget(QLabel("N"))
|
||||
precision_row.addWidget(self.newton_decimals_spin)
|
||||
precision_row.addWidget(QLabel("raw"))
|
||||
precision_row.addWidget(self.raw_decimals_spin)
|
||||
|
||||
# How the finger's 2D canvas is turned for display. A *presentation* choice, so
|
||||
# it lives with the other display ones (this dialog), not with the acquisition
|
||||
@@ -186,6 +198,8 @@ class PlotSettingsDialog(QDialog):
|
||||
form.addRow(self.shear_raw_max_label, self.shear_raw_max_spin)
|
||||
form.addRow(self.deadband_n_label, self.deadband_n_spin)
|
||||
form.addRow(self.deadband_raw_label, self.deadband_raw_spin)
|
||||
self.precision_label = QLabel()
|
||||
form.addRow(self.precision_label, precision_row)
|
||||
root.addLayout(form)
|
||||
|
||||
self.orientation_group = QGroupBox()
|
||||
@@ -438,7 +452,7 @@ class PlotSettingsDialog(QDialog):
|
||||
}
|
||||
|
||||
def deadbands(self) -> dict[str, float]:
|
||||
"""The per-unit display deadband floors. 0 means "auto" (estimated)."""
|
||||
"""The per-unit absolute output thresholds."""
|
||||
return {
|
||||
"deadband_n": float(self.deadband_n_spin.value()),
|
||||
"deadband_raw": float(self.deadband_raw_spin.value()),
|
||||
@@ -446,7 +460,11 @@ class PlotSettingsDialog(QDialog):
|
||||
|
||||
def settings(self) -> dict[str, float]:
|
||||
"""Everything the plot half of the dialog decides, in one mapping."""
|
||||
return {**self.ranges(), **self.deadbands()}
|
||||
return {
|
||||
**self.ranges(), **self.deadbands(),
|
||||
"newton_decimals": int(self.newton_decimals_spin.value()),
|
||||
"raw_decimals": int(self.raw_decimals_spin.value()),
|
||||
}
|
||||
|
||||
def orientation(self) -> str:
|
||||
"""The 2D presentation the dialog is showing (see ``display_orientation``)."""
|
||||
@@ -464,10 +482,10 @@ class PlotSettingsDialog(QDialog):
|
||||
self.orientation_combo.setCurrentIndex(
|
||||
max(self.orientation_combo.findData(DEFAULT_FINGER_ORIENTATION), 0)
|
||||
)
|
||||
# "Auto" is the shipped answer for the deadband: the estimate from the
|
||||
# quiet startup window, with the layer's own floor under it.
|
||||
self.deadband_n_spin.setValue(DEFAULT_PLOT_DEADBAND_N)
|
||||
self.deadband_raw_spin.setValue(DEFAULT_PLOT_DEADBAND_RAW)
|
||||
self.newton_decimals_spin.setValue(DEFAULT_PLOT_NEWTON_DECIMALS)
|
||||
self.raw_decimals_spin.setValue(DEFAULT_PLOT_RAW_DECIMALS)
|
||||
# "Reset" means the shipped defaults, and those filter nothing — the same
|
||||
# answer the dialog gives for a preference file that says nothing.
|
||||
self.flow_ema_spin.setValue(1.0)
|
||||
@@ -498,6 +516,7 @@ class PlotSettingsDialog(QDialog):
|
||||
self.deadband_n_spin.setToolTip(t("dialog.plot_deadband_n_tip"))
|
||||
self.deadband_raw_label.setText(t("dialog.plot_deadband_raw"))
|
||||
self.deadband_raw_spin.setToolTip(t("dialog.plot_deadband_raw_tip"))
|
||||
self.precision_label.setText(t("dialog.plot_value_decimals"))
|
||||
self.btn_reset.setText(t("dialog.reset_defaults"))
|
||||
self.orientation_group.setTitle(t("dialog.plot_orientation_group"))
|
||||
self.orientation_label.setText(t("dialog.plot_orientation"))
|
||||
|
||||
@@ -835,8 +835,7 @@ class ToolbarWidget(QWidget):
|
||||
The operator's complaint was that 重置 read as "reset the charts" when it zeroes
|
||||
the *sensor tracker* — and that in the replay view, where it is the display that
|
||||
can be reset, it was unavailable. The label therefore names the target and the
|
||||
tip states the side effect that is real but invisible: zeroing the tracker also
|
||||
restarts the display baseline (the curve history is kept).
|
||||
tip identifies the target so a tracker reset is not mistaken for a chart reset.
|
||||
"""
|
||||
if self.reset_target() == "display":
|
||||
self.btn_reset.setText(t("toolbar.reset_display"))
|
||||
|
||||
Reference in New Issue
Block a user