Build instructions
This commit is contained in:
444
codex/ARCHITECTURE.md
Normal file
444
codex/ARCHITECTURE.md
Normal file
@@ -0,0 +1,444 @@
|
||||
# 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"
|
||||
```
|
||||
Reference in New Issue
Block a user