445 lines
10 KiB
Markdown
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"
|
|
```
|