Files
dpg-gauges/codex/ARCHITECTURE.md
2026-07-02 12:40:20 +02:00

445 lines
10 KiB
Markdown

# 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"
```