# dpg-gauges Architecture This document defines the internal architecture for `dpg-gauges`. The central rule is: > Dear PyGui item and draw operations happen on the GUI thread. Gauge state is kept separately > from draw commands so high-rate value updates can redraw only the dynamic parts of a gauge. `dpg-gauges` does not need the full thread-safe command architecture used by `dpg-map`; gauge updates are intended to be made from the GUI thread. The package still needs a clean state/render split so animation, smoothing, sizing, and high-rate updates stay stable. ## High-level architecture ```text Public API ↓ Validation and configuration normalization ↓ Gauge registry and GaugeState ↓ Renderer on GUI thread ↓ Dear PyGui child/group + drawlist/text/items ``` ## Critical invariants ### Threading invariants 1. Dear PyGui APIs are called only on the GUI thread. 2. Widget creation functions are GUI-thread-only. 3. Runtime updates are GUI-thread-only unless explicitly documented otherwise. 4. Worker threads may produce raw data, but application code must marshal values to the GUI thread. 5. Locks should not be needed for normal gauge updates in the first release. ### State invariants 1. Every gauge has one `GaugeState` stored in the registry. 2. Public tags identify logical gauges and should align with Dear PyGui behaviour where practical. 3. Raw target value, clamped value, and displayed animated value are separate concepts. 4. Thresholds and markers are configuration, not business logic. 5. Out-of-range values clamp before rendering. 6. Non-finite values enter an invalid display state instead of crashing. ### Rendering invariants 1. Static geometry and dynamic geometry should be separate where practical. 2. Value updates should not rebuild static ticks, labels, threshold bands, or frames unless needed. 3. Configuration changes mark the smallest reasonable dirty set. 4. Redraws must not flicker through an empty state. 5. Hidden or zero-size layouts must not permanently collapse a gauge. ### Sizing invariants 1. Requested size and measured size are stored separately. 2. Drawlists use concrete measured dimensions. 3. A measured size of zero while hidden does not overwrite the last useful non-zero size. 4. Square gauges use the smaller dimension by default. 5. Stretchable gauges can use the full available width/height. ## Package layout ```text src/dpg_gauges/ __init__.py api.py widget.py state.py renderer.py draw_layers.py sizing.py animation.py smoothing.py scales.py themes.py gauges.py diagnostics.py types.py exceptions.py examples/ basic_analog.py basic_digital.py basic_bar.py basic_level.py basic_status.py basic_segmented.py basic_compass.py basic_thermometer.py basic_battery.py basic_multi_value.py thresholds_markers.py animations.py dashboard.py sizing.py live_high_rate.py live_worst_case.py tests/ docs/ GETTING_STARTED.md EXAMPLES.md API_REFERENCE.md codex/ README.md FEATURES.md ARCHITECTURE.md STEPS.md AGENTS.md ``` ## Module responsibilities ### `__init__.py` Exports the public API only. No expensive Dear PyGui initialization should happen here. ### `api.py` Contains thin public wrappers: - resolve gauge tags - validate and normalize simple arguments - call creation helpers - call state update helpers - expose stable runtime functions `api.py` should not contain detailed drawing code. ### `types.py` Defines shared types and dataclasses: ```python Tag = str | int Color = tuple[int, int, int] | tuple[int, int, int, int] GaugeValue = float | int | bool | str | None @dataclass(frozen=True) class ThresholdBand: ... @dataclass(frozen=True) class GaugeMarker: ... @dataclass(frozen=True) class AnimationConfig: ... @dataclass(frozen=True) class SmoothingConfig: ... @dataclass(frozen=True) class GaugeStats: ... ``` ### `exceptions.py` Public exceptions: ```python class DpgGaugesError(Exception): ... class GaugeNotFoundError(DpgGaugesError): ... class GaugeConfigError(DpgGaugesError): ... class GaugeValueError(DpgGaugesError): ... class GaugeTypeError(DpgGaugesError): ... class ThreadingError(DpgGaugesError): ... ``` ### `state.py` Owns global configuration, the gauge registry, and `GaugeState`. ```python @dataclass class DpgGaugesConfig: default_theme: str | GaugeTheme | None = None animation: AnimationConfig | bool | None = None smoothing: SmoothingConfig | bool | None = None default_width: int | None = None default_height: int | None = None frame_rate_limit: float | None = None debug: bool = False @dataclass class GaugeState: tag: Tag gauge_type: str container_tag: Tag drawlist_tag: Tag | None text_tags: dict[str, Tag] config: GaugeConfig target_value: GaugeValue clamped_value: float | None displayed_value: float | None previous_displayed_value: float | None value_revision: int = 0 config_revision: int = 0 measured_width: int = 0 measured_height: int = 0 last_nonzero_width: int = 0 last_nonzero_height: int = 0 dirty: DirtyFlags = DirtyFlags.FULL renderer: GaugeRenderer | None = None ``` ### `gauges.py` Defines normalized configuration dataclasses for gauge families: - `GaugeConfig` - `AnalogGaugeConfig` - `DigitalGaugeConfig` - `BarGaugeConfig` - `LevelGaugeConfig` - `StatusLightConfig` - `SegmentedGaugeConfig` - `CompassGaugeConfig` - `ThermometerGaugeConfig` - `BatteryGaugeConfig` - `MultiValueGaugeConfig` This module should be mostly pure and easy to test. ### `widget.py` Creates Dear PyGui items for gauges. Typical internal structure: ```text group or child_window drawlist optional text/value items ``` Rules: - Creation functions enter a context manager when appropriate. - The logical gauge is registered before yielding. - Raw internal draw item tags are not required as public API. - Callback and tooltip wiring happens here or in a small helper. ### `renderer.py` GUI-thread-only renderer. Responsibilities: - measure gauge size - apply animation/smoothing progression - rebuild dirty static layer - rebuild dirty dynamic layer - update text/value items - avoid unnecessary static redraws - expose debug state Gauge rendering should prefer Dear PyGui drawlists for shapes, arcs, needles, bars, and custom visuals. Plain Dear PyGui text items may be used when that is simpler and more reliable. ### `draw_layers.py` Tracks draw item tags by purpose: ```python @dataclass class DrawLayer: name: str item_tags: set[Tag] = field(default_factory=set) ``` Suggested layers: - background - static scale - thresholds - dynamic value - markers - text/debug The renderer can delete and rebuild only selected layers. ### `sizing.py` Provides helpers for measured dimensions and aspect decisions: - track requested vs measured size - preserve last non-zero size - calculate square gauge rect - calculate stretch rect - detect hidden-to-visible transitions ### `animation.py` Provides animation interpolation: - linear interpolation - simple easing names - target/displayed value separation - frame scheduling rules Avoid an overbuilt animation framework in the first release. ### `smoothing.py` Provides optional readability smoothing: - exponential moving average via `alpha` - optional `max_step` - reset behaviour when value jumps or gauge is reconfigured ### `scales.py` Pure math helpers: - clamp and normalize values - map value to angle - map value to bar fill fraction - generate tick values - format values - resolve threshold geometry No Dear PyGui imports. ### `themes.py` Lightweight presets and style normalization. This should complement Dear PyGui theming, not replace it. Built-in presets can include: - `default` - `dark` - `minimal` - `motorsport` - `industrial` ### `diagnostics.py` Provides: ```python def get_gauge_debug_state(tag) -> dict[str, Any]: ... ``` Include: - gauge type - target value - clamped value - displayed value - measured size - last non-zero size - dirty flags - value/config revisions - animation state - smoothing state ## Dirty flag model Use bit flags: ```python class DirtyFlags(IntFlag): NONE = 0 VALUE = auto() STATIC = auto() DYNAMIC = auto() TEXT = auto() SIZE = auto() STYLE = auto() DEBUG = auto() FULL = VALUE | STATIC | DYNAMIC | TEXT | SIZE | STYLE | DEBUG ``` Expected usage: | Operation | Dirty flags | |---|---| | value update | `VALUE | DYNAMIC | TEXT` | | animation tick | `DYNAMIC | TEXT` | | threshold update | `STATIC | DYNAMIC` | | marker update | `STATIC | DYNAMIC` | | label/unit/precision update | `TEXT` | | min/max update | `STATIC | DYNAMIC | TEXT` | | resize | `SIZE | STATIC | DYNAMIC | TEXT` | | style/theme update | `STYLE | STATIC | DYNAMIC | TEXT` | ## Frame and update lifecycle For each gauge update: ```text 1. Public API validates value/config. 2. State target value/config is updated. 3. Dirty flags are marked. 4. Renderer is scheduled if needed. 5. Renderer measures size. 6. Renderer advances animation/smoothing. 7. Renderer redraws static layers only if needed. 8. Renderer redraws dynamic/text layers as needed. 9. Dirty flags are cleared. 10. Renderer continues scheduling frames only while animation/smoothing is active. ``` ## High-rate telemetry strategy The first release should not promise background-thread-safe UI calls. Instead, examples should show the correct Dear PyGui pattern: ```text producer thread writes latest raw value to a lock-protected variable or queue GUI frame callback reads the newest value GUI frame callback calls dpgg.set_value(...) gauge renderer coalesces/animates/smooths visual changes ``` This preserves Dear PyGui thread rules while still covering worst-case telemetry rates. ## Initial setup instructions ```bash mkdir dpg-gauges cd dpg-gauges git init uv init --package dpg-gauges uv add dearpygui uv add --dev pytest ruff pyright ``` Recommended `pyproject.toml` additions: ```toml [tool.ruff] line-length = 100 target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM"] [tool.pyright] typeCheckingMode = "basic" ```