step 6: add animation and high rate update stress tests
This commit is contained in:
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
Dear PyGui gauge widgets for dashboards and telemetry displays.
|
Dear PyGui gauge widgets for dashboards and telemetry displays.
|
||||||
|
|
||||||
Current implementation status: Step 5 first catalogue rendering is implemented. Analog, digital,
|
Current implementation status: Step 6 high-rate update support is implemented. Analog, digital, bar,
|
||||||
bar, level, status, segmented, compass, thermometer, battery, and multi-value gauges render Dear
|
level, status, segmented, compass, thermometer, battery, and multi-value gauges render Dear PyGui
|
||||||
PyGui drawlist visuals. Bar/level gauges draw threshold bands and markers; analog gauges draw ticks,
|
drawlist visuals. Bar/level gauges draw threshold bands and markers; analog gauges draw ticks,
|
||||||
redline bands, markers, and needles.
|
redline bands, markers, and needles.
|
||||||
|
|
||||||
Runtime value updates are intended for the Dear PyGui GUI thread. Attached gauges redraw immediately
|
Runtime value updates are intended for the Dear PyGui GUI thread. Attached gauges redraw immediately
|
||||||
for simple value/configuration changes.
|
for simple value/configuration changes. Animation and smoothing are configurable per gauge; high-rate
|
||||||
|
examples marshal latest producer-thread values to GUI-thread frame callbacks.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
Step 5 completed and verified with automated checks. Step 5 commit is pending.
|
Step 6 completed and verified with automated checks. Step 6 commit is pending.
|
||||||
|
|
||||||
## Completed steps
|
## Completed steps
|
||||||
|
|
||||||
@@ -28,7 +28,19 @@ Step 1 - Public API contract and pure core:
|
|||||||
|
|
||||||
## Current step
|
## Current step
|
||||||
|
|
||||||
Step 6 - High-rate updates, animation, and stress examples.
|
Step 7 - Documentation, hardening, and beta release.
|
||||||
|
|
||||||
|
Step 6 - High-rate updates, animation, and stress examples:
|
||||||
|
|
||||||
|
Implemented during Step 6:
|
||||||
|
|
||||||
|
- Runtime updates now start animations using monotonic time for public calls.
|
||||||
|
- Optional smoothing now progresses displayed values toward the latest target instead of snapping immediately.
|
||||||
|
- Repeated GUI-thread value updates coalesce naturally to the newest target with no explicit queue.
|
||||||
|
- Renderer frame scheduling remains guarded so continuous callbacks are scheduled only while animation or smoothing is active.
|
||||||
|
- Debug state now includes detailed animation and smoothing fields.
|
||||||
|
- Added `examples/animations.py`, `examples/live_high_rate.py`, and `examples/live_worst_case.py` using GUI-thread frame callbacks for Dear PyGui updates.
|
||||||
|
- Added Step 6 tests for animation convergence, smoothing reset behaviour, latest-target wins, and renderer scheduling.
|
||||||
|
|
||||||
Step 5 - Analog, segmented, compass, thermometer, battery, and multi-value gauges:
|
Step 5 - Analog, segmented, compass, thermometer, battery, and multi-value gauges:
|
||||||
|
|
||||||
@@ -91,7 +103,7 @@ Implemented during Step 2:
|
|||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
Manual visual example checks for Steps 3, 4, and 5 were not run in this headless session.
|
Manual visual/stress example checks for Steps 3, 4, 5, and 6 were not run in this headless session.
|
||||||
|
|
||||||
## Commands used
|
## Commands used
|
||||||
|
|
||||||
@@ -118,6 +130,14 @@ Manual visual example checks for Steps 3, 4, and 5 were not run in this headless
|
|||||||
- Ran `uv run ruff format .`.
|
- Ran `uv run ruff format .`.
|
||||||
- Ran `uv run ruff format --check .`.
|
- Ran `uv run ruff format --check .`.
|
||||||
- Ran `uv run pyright`.
|
- Ran `uv run pyright`.
|
||||||
|
- Committed Step 5 with `git commit -m "step 5: complete first gauge catalogue"` before starting Step 6.
|
||||||
|
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 6 changes.
|
||||||
|
- Implemented Step 6 animation/smoothing scheduling refinements, high-rate examples, README status update, diagnostics fields, and tests.
|
||||||
|
- Ran `uv run pytest`.
|
||||||
|
- Ran `uv run ruff check .`.
|
||||||
|
- Ran `uv run ruff format .`.
|
||||||
|
- Ran `uv run ruff format --check .`.
|
||||||
|
- Ran `uv run pyright`.
|
||||||
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 2 changes.
|
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 2 changes.
|
||||||
- Implemented Step 2 state, sizing, animation, smoothing, runtime API, diagnostics, tests, and README status update.
|
- Implemented Step 2 state, sizing, animation, smoothing, runtime API, diagnostics, tests, and README status update.
|
||||||
- Ran `uv run pytest`.
|
- Ran `uv run pytest`.
|
||||||
@@ -142,4 +162,4 @@ Manual visual example checks for Steps 3, 4, and 5 were not run in this headless
|
|||||||
|
|
||||||
## Next action
|
## Next action
|
||||||
|
|
||||||
Commit Step 5, then implement Step 6.
|
Commit Step 6, then implement Step 7.
|
||||||
|
|||||||
51
examples/animations.py
Normal file
51
examples/animations.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
|
import dpg_gauges as dpgg
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
dpg.create_context()
|
||||||
|
started = time.monotonic()
|
||||||
|
|
||||||
|
def update() -> None:
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
dpgg.set_value("animated", 50 + math.sin(elapsed * 2.0) * 50)
|
||||||
|
dpgg.set_value("smoothed", 50 + math.sin(elapsed * 7.0) * 50)
|
||||||
|
dpg.set_frame_callback(dpg.get_frame_count() + 1, update)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with dpg.window(label="Animations", width=460, height=260):
|
||||||
|
dpgg.digital_gauge(
|
||||||
|
tag="animated",
|
||||||
|
label="Animated",
|
||||||
|
value=0,
|
||||||
|
width=360,
|
||||||
|
height=90,
|
||||||
|
animation=dpgg.AnimationConfig(enabled=True, duration=0.35),
|
||||||
|
)
|
||||||
|
dpgg.bar_gauge(
|
||||||
|
tag="smoothed",
|
||||||
|
label="Smoothed",
|
||||||
|
value=0,
|
||||||
|
width=360,
|
||||||
|
height=90,
|
||||||
|
animation=False,
|
||||||
|
smoothing=dpgg.SmoothingConfig(enabled=True, alpha=0.18, max_step=8),
|
||||||
|
)
|
||||||
|
|
||||||
|
dpg.create_viewport(title="dpg-gauges animations", width=500, height=300)
|
||||||
|
dpg.setup_dearpygui()
|
||||||
|
dpg.set_frame_callback(1, update)
|
||||||
|
dpg.show_viewport()
|
||||||
|
dpg.start_dearpygui()
|
||||||
|
finally:
|
||||||
|
dpg.destroy_context()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
73
examples/live_high_rate.py
Normal file
73
examples/live_high_rate.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
|
||||||
|
import math
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
|
import dpg_gauges as dpgg
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
latest = {"rpm": 0.0, "speed": 0.0}
|
||||||
|
lock = threading.Lock()
|
||||||
|
stop = threading.Event()
|
||||||
|
|
||||||
|
def producer() -> None:
|
||||||
|
started = time.monotonic()
|
||||||
|
while not stop.is_set():
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
with lock:
|
||||||
|
latest["rpm"] = 4000 + math.sin(elapsed * 9.0) * 3500
|
||||||
|
latest["speed"] = 120 + math.sin(elapsed * 3.0) * 80
|
||||||
|
time.sleep(1 / 240)
|
||||||
|
|
||||||
|
def gui_update() -> None:
|
||||||
|
with lock:
|
||||||
|
rpm = latest["rpm"]
|
||||||
|
speed = latest["speed"]
|
||||||
|
dpgg.set_value("rpm", rpm)
|
||||||
|
dpgg.set_value("speed", speed)
|
||||||
|
dpg.set_frame_callback(dpg.get_frame_count() + 1, gui_update)
|
||||||
|
|
||||||
|
dpg.create_context()
|
||||||
|
thread = threading.Thread(target=producer, daemon=True)
|
||||||
|
try:
|
||||||
|
with dpg.window(label="High Rate", width=620, height=360):
|
||||||
|
dpgg.analog_gauge(
|
||||||
|
tag="rpm",
|
||||||
|
label="RPM",
|
||||||
|
value=0,
|
||||||
|
max_value=8000,
|
||||||
|
redline_start=6500,
|
||||||
|
width=240,
|
||||||
|
height=240,
|
||||||
|
animation=False,
|
||||||
|
smoothing=dpgg.SmoothingConfig(enabled=True, alpha=0.22, max_step=500),
|
||||||
|
)
|
||||||
|
dpgg.digital_gauge(
|
||||||
|
tag="speed",
|
||||||
|
label="Speed",
|
||||||
|
value=0,
|
||||||
|
max_value=240,
|
||||||
|
unit="km/h",
|
||||||
|
width=260,
|
||||||
|
height=90,
|
||||||
|
animation=dpgg.AnimationConfig(enabled=True, duration=0.12),
|
||||||
|
)
|
||||||
|
|
||||||
|
dpg.create_viewport(title="dpg-gauges high rate", width=660, height=400)
|
||||||
|
dpg.setup_dearpygui()
|
||||||
|
thread.start()
|
||||||
|
dpg.set_frame_callback(1, gui_update)
|
||||||
|
dpg.show_viewport()
|
||||||
|
dpg.start_dearpygui()
|
||||||
|
finally:
|
||||||
|
stop.set()
|
||||||
|
thread.join(timeout=1.0)
|
||||||
|
dpg.destroy_context()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
58
examples/live_worst_case.py
Normal file
58
examples/live_worst_case.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
|
import dpg_gauges as dpgg
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
latest = {"rpm": 0.0, "boost": 0.0, "fuel": 0.0, "battery": 0.0}
|
||||||
|
lock = threading.Lock()
|
||||||
|
stop = threading.Event()
|
||||||
|
|
||||||
|
def producer() -> None:
|
||||||
|
started = time.monotonic()
|
||||||
|
while not stop.is_set():
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
with lock:
|
||||||
|
latest["rpm"] = random.uniform(0, 8000)
|
||||||
|
latest["boost"] = 15 + math.sin(elapsed * 25.0) * 15
|
||||||
|
latest["fuel"] = 50 + math.sin(elapsed * 4.0) * 50
|
||||||
|
latest["battery"] = random.uniform(0, 100)
|
||||||
|
time.sleep(1 / 500)
|
||||||
|
|
||||||
|
def gui_update() -> None:
|
||||||
|
with lock:
|
||||||
|
snapshot = dict(latest)
|
||||||
|
for tag, value in snapshot.items():
|
||||||
|
dpgg.set_value(tag, value)
|
||||||
|
dpg.set_frame_callback(dpg.get_frame_count() + 1, gui_update)
|
||||||
|
|
||||||
|
dpg.create_context()
|
||||||
|
thread = threading.Thread(target=producer, daemon=True)
|
||||||
|
try:
|
||||||
|
with dpg.window(label="Worst Case", width=760, height=420), dpgg.gauge_grid(columns=4):
|
||||||
|
dpgg.analog_gauge(tag="rpm", label="RPM", max_value=8000, width=180, height=180)
|
||||||
|
dpgg.bar_gauge(tag="boost", label="Boost", max_value=30, width=180, height=90)
|
||||||
|
dpgg.level_gauge(tag="fuel", label="Fuel", width=120, height=180)
|
||||||
|
dpgg.battery_gauge(tag="battery", label="Battery", width=180, height=90)
|
||||||
|
|
||||||
|
dpg.create_viewport(title="dpg-gauges worst case", width=800, height=460)
|
||||||
|
dpg.setup_dearpygui()
|
||||||
|
thread.start()
|
||||||
|
dpg.set_frame_callback(1, gui_update)
|
||||||
|
dpg.show_viewport()
|
||||||
|
dpg.start_dearpygui()
|
||||||
|
finally:
|
||||||
|
stop.set()
|
||||||
|
thread.join(timeout=1.0)
|
||||||
|
dpg.destroy_context()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -25,6 +25,17 @@ def get_gauge_debug_state(tag: Tag) -> dict[str, Any]:
|
|||||||
"config_revision": state.config_revision,
|
"config_revision": state.config_revision,
|
||||||
"animation_active": state.animation_state.active,
|
"animation_active": state.animation_state.active,
|
||||||
"smoothing_active": state.smoothing_state.active,
|
"smoothing_active": state.smoothing_state.active,
|
||||||
|
"animation": {
|
||||||
|
"start_value": state.animation_state.start_value,
|
||||||
|
"target_value": state.animation_state.target_value,
|
||||||
|
"started_at": state.animation_state.started_at,
|
||||||
|
"duration": state.animation_state.duration,
|
||||||
|
"active": state.animation_state.active,
|
||||||
|
},
|
||||||
|
"smoothing": {
|
||||||
|
"value": state.smoothing_state.value,
|
||||||
|
"active": state.smoothing_state.active,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if state.renderer is not None:
|
if state.renderer is not None:
|
||||||
debug["renderer"] = state.renderer.debug_state()
|
debug["renderer"] = state.renderer.debug_state()
|
||||||
|
|||||||
@@ -31,9 +31,16 @@ from .scales import (
|
|||||||
)
|
)
|
||||||
from .sizing import effective_size, square_gauge_rect, update_measured_size
|
from .sizing import effective_size, square_gauge_rect, update_measured_size
|
||||||
from .smoothing import advance_smoothing
|
from .smoothing import advance_smoothing
|
||||||
from .state import DirtyFlags, GaugeState, animation_config, size_state_for_gauge, update_state_size
|
from .state import (
|
||||||
|
DirtyFlags,
|
||||||
|
GaugeState,
|
||||||
|
animation_config,
|
||||||
|
size_state_for_gauge,
|
||||||
|
smoothing_config,
|
||||||
|
update_state_size,
|
||||||
|
)
|
||||||
from .themes import GaugeStyle
|
from .themes import GaugeStyle
|
||||||
from .types import SmoothingConfig, StatusState, Tag
|
from .types import StatusState, Tag
|
||||||
|
|
||||||
Rect = tuple[float, float, float, float]
|
Rect = tuple[float, float, float, float]
|
||||||
|
|
||||||
@@ -97,7 +104,7 @@ class GaugeRenderer:
|
|||||||
if state.animation_state.active:
|
if state.animation_state.active:
|
||||||
state.displayed_value, active = advance_animation(state.animation_state, config, now)
|
state.displayed_value, active = advance_animation(state.animation_state, config, now)
|
||||||
state.animation_state.active = active
|
state.animation_state.active = active
|
||||||
smoothing = _smoothing_config(state)
|
smoothing = smoothing_config(state.config)
|
||||||
if not state.animation_state.active and smoothing is not None and smoothing.enabled:
|
if not state.animation_state.active and smoothing is not None and smoothing.enabled:
|
||||||
state.displayed_value, _ = advance_smoothing(
|
state.displayed_value, _ = advance_smoothing(
|
||||||
state.smoothing_state, state.clamped_value, smoothing
|
state.smoothing_state, state.clamped_value, smoothing
|
||||||
@@ -640,13 +647,6 @@ def render_attached_gauge(state: GaugeState) -> None:
|
|||||||
state.renderer.render()
|
state.renderer.render()
|
||||||
|
|
||||||
|
|
||||||
def _smoothing_config(state: GaugeState) -> SmoothingConfig | None:
|
|
||||||
smoothing = state.config.smoothing
|
|
||||||
if isinstance(smoothing, bool):
|
|
||||||
return SmoothingConfig(enabled=smoothing)
|
|
||||||
return smoothing
|
|
||||||
|
|
||||||
|
|
||||||
def _drawlist_tag(state: GaugeState) -> Tag:
|
def _drawlist_tag(state: GaugeState) -> Tag:
|
||||||
return cast(Tag, state.drawlist_tag)
|
return cast(Tag, state.drawlist_tag)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
import time
|
||||||
from dataclasses import dataclass, field, replace
|
from dataclasses import dataclass, field, replace
|
||||||
from enum import IntFlag, auto
|
from enum import IntFlag, auto
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -144,6 +145,13 @@ def animation_config(config: GaugeConfig) -> AnimationConfig | None:
|
|||||||
return animation
|
return animation
|
||||||
|
|
||||||
|
|
||||||
|
def smoothing_config(config: GaugeConfig) -> SmoothingConfig | None:
|
||||||
|
smoothing = config.smoothing
|
||||||
|
if isinstance(smoothing, bool):
|
||||||
|
return SmoothingConfig(enabled=smoothing)
|
||||||
|
return smoothing
|
||||||
|
|
||||||
|
|
||||||
def clamp_runtime_value(value: GaugeValue, config: GaugeConfig) -> tuple[float | None, bool]:
|
def clamp_runtime_value(value: GaugeValue, config: GaugeConfig) -> tuple[float | None, bool]:
|
||||||
numeric = coerce_numeric_value(value)
|
numeric = coerce_numeric_value(value)
|
||||||
if numeric is None:
|
if numeric is None:
|
||||||
@@ -203,8 +211,9 @@ def get_gauge_state(tag: Tag) -> GaugeState:
|
|||||||
raise GaugeNotFoundError(msg) from exc
|
raise GaugeNotFoundError(msg) from exc
|
||||||
|
|
||||||
|
|
||||||
def set_runtime_value(tag: Tag, value: GaugeValue, *, now: float = 0.0) -> None:
|
def set_runtime_value(tag: Tag, value: GaugeValue, *, now: float | None = None) -> None:
|
||||||
state = get_gauge_state(tag)
|
state = get_gauge_state(tag)
|
||||||
|
now = time.monotonic() if now is None else now
|
||||||
clamped, invalid = clamp_runtime_value(value, state.config)
|
clamped, invalid = clamp_runtime_value(value, state.config)
|
||||||
state.target_value = value
|
state.target_value = value
|
||||||
state.clamped_value = clamped
|
state.clamped_value = clamped
|
||||||
@@ -215,8 +224,15 @@ def set_runtime_value(tag: Tag, value: GaugeValue, *, now: float = 0.0) -> None:
|
|||||||
state.displayed_value, clamped, animation_config(state.config), now
|
state.displayed_value, clamped, animation_config(state.config), now
|
||||||
)
|
)
|
||||||
if not state.animation_state.active:
|
if not state.animation_state.active:
|
||||||
state.displayed_value = clamped
|
smoothing = smoothing_config(state.config)
|
||||||
state.smoothing_state.value = clamped
|
if smoothing is not None and smoothing.enabled and clamped is not None and not invalid:
|
||||||
|
if state.smoothing_state.value is None:
|
||||||
|
state.smoothing_state.value = state.previous_displayed_value
|
||||||
|
state.smoothing_state.active = state.smoothing_state.value != clamped
|
||||||
|
else:
|
||||||
|
state.displayed_value = clamped
|
||||||
|
state.smoothing_state.value = clamped
|
||||||
|
state.smoothing_state.active = False
|
||||||
state.dirty |= DirtyFlags.VALUE | DirtyFlags.DYNAMIC | DirtyFlags.TEXT
|
state.dirty |= DirtyFlags.VALUE | DirtyFlags.DYNAMIC | DirtyFlags.TEXT
|
||||||
render_runtime_gauge(state)
|
render_runtime_gauge(state)
|
||||||
|
|
||||||
|
|||||||
101
tests/test_step6_animation_stress.py
Normal file
101
tests/test_step6_animation_stress.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import dpg_gauges as dpgg
|
||||||
|
from dpg_gauges.animation import advance_animation
|
||||||
|
from dpg_gauges.state import clear_registry, get_gauge_state, set_runtime_value
|
||||||
|
from dpg_gauges.types import AnimationConfig, SmoothingConfig
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dpg_context() -> Generator[None]:
|
||||||
|
clear_registry()
|
||||||
|
dpgg.configure()
|
||||||
|
dpg.create_context()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
clear_registry()
|
||||||
|
dpg.destroy_context()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def clean_registry() -> Generator[None]:
|
||||||
|
clear_registry()
|
||||||
|
dpgg.configure()
|
||||||
|
yield
|
||||||
|
clear_registry()
|
||||||
|
|
||||||
|
|
||||||
|
def test_animation_converges_to_latest_target() -> None:
|
||||||
|
dpgg.digital_gauge(tag="anim", value=0, animation=AnimationConfig(enabled=True, duration=1.0))
|
||||||
|
|
||||||
|
set_runtime_value("anim", 100, now=10.0)
|
||||||
|
state = get_gauge_state("anim")
|
||||||
|
config = state.config.animation
|
||||||
|
assert isinstance(config, AnimationConfig)
|
||||||
|
value, active = advance_animation(state.animation_state, config, now=10.5)
|
||||||
|
assert value == 50
|
||||||
|
assert active is True
|
||||||
|
|
||||||
|
value, active = advance_animation(state.animation_state, config, now=11.0)
|
||||||
|
assert value == 100
|
||||||
|
assert active is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_smoothing_progresses_and_resets_on_range_change() -> None:
|
||||||
|
dpgg.digital_gauge(
|
||||||
|
tag="smooth",
|
||||||
|
value=0,
|
||||||
|
smoothing=SmoothingConfig(enabled=True, alpha=0.5),
|
||||||
|
)
|
||||||
|
|
||||||
|
set_runtime_value("smooth", 100, now=1.0)
|
||||||
|
state = get_gauge_state("smooth")
|
||||||
|
assert state.displayed_value == 0
|
||||||
|
assert state.smoothing_state.active is True
|
||||||
|
|
||||||
|
dpgg.configure_gauge("smooth", max_value=50)
|
||||||
|
assert state.displayed_value == 50
|
||||||
|
assert state.smoothing_state.value == 50
|
||||||
|
|
||||||
|
|
||||||
|
def test_latest_target_wins_without_queue_growth() -> None:
|
||||||
|
dpgg.digital_gauge(tag="latest", value=0, animation=AnimationConfig(enabled=True, duration=1.0))
|
||||||
|
|
||||||
|
set_runtime_value("latest", 25, now=1.0)
|
||||||
|
set_runtime_value("latest", 75, now=1.1)
|
||||||
|
set_runtime_value("latest", 50, now=1.2)
|
||||||
|
|
||||||
|
state = get_gauge_state("latest")
|
||||||
|
assert state.target_value == 50
|
||||||
|
assert state.clamped_value == 50
|
||||||
|
assert state.animation_state.target_value == 50
|
||||||
|
assert state.value_revision == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_attached_renderer_schedules_only_while_animation_active(dpg_context: None) -> None:
|
||||||
|
with dpg.window(label="Animation"):
|
||||||
|
dpgg.digital_gauge(
|
||||||
|
tag="attached",
|
||||||
|
value=0,
|
||||||
|
width=180,
|
||||||
|
height=80,
|
||||||
|
animation=AnimationConfig(enabled=True, duration=1.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
dpgg.set_value("attached", 100)
|
||||||
|
debug = dpgg.get_gauge_debug_state("attached")
|
||||||
|
assert debug["renderer"]["scheduled"] is True
|
||||||
|
assert debug["animation"]["target_value"] == 100
|
||||||
|
|
||||||
|
state = get_gauge_state("attached")
|
||||||
|
state.animation_state.active = False
|
||||||
|
state.smoothing_state.active = False
|
||||||
|
state.renderer._scheduled = False
|
||||||
|
state.renderer.render()
|
||||||
|
assert dpgg.get_gauge_debug_state("attached")["renderer"]["scheduled"] is False
|
||||||
Reference in New Issue
Block a user