step 2: add gauge state sizing and animation model

This commit is contained in:
2026-07-02 13:54:58 +02:00
parent 44e4cbd246
commit 34fd60c34e
27 changed files with 1732 additions and 5 deletions

View File

@@ -0,0 +1,162 @@
import pytest
import dpg_gauges as dpgg
from dpg_gauges.animation import advance_animation, interpolate, start_animation
from dpg_gauges.exceptions import GaugeConfigError, GaugeNotFoundError
from dpg_gauges.gauges import DigitalGaugeConfig
from dpg_gauges.sizing import (
SizeState,
effective_size,
requested_size,
square_gauge_rect,
stretch_gauge_rect,
update_measured_size,
)
from dpg_gauges.smoothing import SmoothingState, advance_smoothing, smooth_value
from dpg_gauges.state import DirtyFlags, clear_registry, get_gauge_state, register_gauge
from dpg_gauges.types import AnimationConfig, SmoothingConfig
@pytest.fixture(autouse=True)
def clean_registry() -> None:
clear_registry()
dpgg.configure()
def test_registry_register_get_and_delete() -> None:
with dpgg.digital_gauge(tag="speed", value=42, min_value=0, max_value=100) as config:
assert config.tag == "speed"
assert dpgg.get_value("speed") == 42
state = get_gauge_state("speed")
assert state.gauge_type == "digital"
assert state.clamped_value == 42
assert state.displayed_value == 42
with pytest.raises(GaugeConfigError):
register_gauge("digital", DigitalGaugeConfig(tag="speed"))
dpgg.delete_gauge("speed")
with pytest.raises(GaugeNotFoundError):
dpgg.get_value("speed")
def test_value_clamped_displayed_and_dirty_flags_are_separate() -> None:
dpgg.digital_gauge(tag="rpm", value=10, min_value=0, max_value=100, animation=True)
state = get_gauge_state("rpm")
state.dirty = DirtyFlags.NONE
dpgg.set_value("rpm", 150)
assert dpgg.get_value("rpm") == 150
assert state.clamped_value == 100
assert state.displayed_value == 10
assert state.animation_state.active
assert state.dirty & DirtyFlags.VALUE
assert state.dirty & DirtyFlags.DYNAMIC
assert state.dirty & DirtyFlags.TEXT
def test_update_gauge_marks_static_text_size_and_style_dirty() -> None:
dpgg.digital_gauge(tag="temp", value=20, min_value=0, max_value=100)
state = get_gauge_state("temp")
state.dirty = DirtyFlags.NONE
dpgg.update_gauge(
"temp",
label="Temperature",
unit="C",
min_value=-40,
max_value=140,
width=200,
style=state.config.style,
)
assert state.config.label == "Temperature"
assert state.config.unit == "C"
assert state.config.min_value == -40
assert state.dirty & DirtyFlags.STATIC
assert state.dirty & DirtyFlags.DYNAMIC
assert state.dirty & DirtyFlags.TEXT
assert state.dirty & DirtyFlags.SIZE
assert state.dirty & DirtyFlags.STYLE
def test_threshold_marker_and_show_runtime_updates() -> None:
dpgg.digital_gauge(tag="fuel", min_value=0, max_value=100)
threshold = dpgg.ThresholdBand(0, 15, (255, 0, 0))
marker = dpgg.GaugeMarker(50, (0, 255, 0))
dpgg.set_thresholds("fuel", [threshold])
dpgg.set_markers("fuel", [marker])
dpgg.set_show("fuel", False)
state = get_gauge_state("fuel")
assert state.config.thresholds == (threshold,)
assert state.config.markers == (marker,)
assert state.config.show is False
assert state.dirty & DirtyFlags.STATIC
assert state.dirty & DirtyFlags.SIZE
def test_non_finite_values_do_not_crash_state_helpers() -> None:
dpgg.digital_gauge(tag="boost", min_value=0, max_value=30)
dpgg.set_value("boost", float("nan"))
state = get_gauge_state("boost")
assert state.target_value != state.target_value
assert state.clamped_value is None
assert state.displayed_value is None
assert state.invalid_value is True
def test_sizing_preserves_last_nonzero_size_and_rects() -> None:
size = SizeState(requested=requested_size(100, 80))
assert update_measured_size(size, 320, 180) is True
assert effective_size(size) == (320, 180)
update_measured_size(size, 0, 0)
assert effective_size(size) == (320, 180)
assert square_gauge_rect(320, 180).width == 180
assert square_gauge_rect(320, 180).x == 70
assert stretch_gauge_rect(320, 180).width == 320
assert stretch_gauge_rect(320, 180).height == 180
def test_animation_interpolation_and_completion() -> None:
config = AnimationConfig(enabled=True, duration=1.0)
state = start_animation(0, 100, config, now=10.0)
assert interpolate(0, 100, 0.25) == 25
value, active = advance_animation(state, config, now=10.5)
assert value == 50
assert active is True
value, active = advance_animation(state, config, now=11.0)
assert value == 100
assert active is False
def test_smoothing_progression() -> None:
config = SmoothingConfig(enabled=True, alpha=0.5, max_step=10)
assert smooth_value(None, 100, config) == (100, False)
state = SmoothingState(value=0)
value, active = advance_smoothing(state, 100, config)
assert value == 10
assert active is True
def test_debug_state_reports_model_fields() -> None:
dpgg.digital_gauge(tag="debug", value=5, width=120, height=30)
debug = dpgg.get_gauge_debug_state("debug")
assert debug["gauge_type"] == "digital"
assert debug["target_value"] == 5
assert debug["clamped_value"] == 5
assert debug["measured_size"] == (120, 30)
assert "VALUE" in debug["dirty_names"]