# Elodin Editor Dev

> Contribute to the Elodin Editor, the 3D viewer and graphing tool. Use when editing files in libs/elodin-editor/ or apps/elodin/, working on the Bevy/Egui UI, modifying viewport rendering, telemetry graphs, video streaming, KDL schematics, or the command palette.

- Skill: `elodin-sys/elodin-editor-dev` (Agent Skill)
- Install (CLI): `npx skillmds@latest add elodin-sys/elodin-editor-dev`
- Raw SKILL.md: https://api.skillmd.com/api/skills/elodin-sys/elodin-editor-dev/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: elodin-sys (https://skillmd.com/u/elodin-sys)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/elodin-sys/elodin-editor-dev

---


# Elodin Editor Development

The Elodin Editor is a 3D visualization and telemetry graphing tool built with Bevy (ECS game engine) and Egui (immediate-mode UI). It connects to Elodin-DB via Impeller2 for real-time data.

## Running

```bash
# Standard
cargo run --bin elodin editor examples/three-body/main.py

# With hot-reload on editor code changes (requires cargo-watch)
cargo watch --watch libs/elodin-editor \
    -x 'run --bin elodin editor examples/three-body/main.py'

# Connect to a running database instead of a simulation
cargo run --bin elodin editor 127.0.0.1:2240
```

## Environment Variables

| Variable | Default | Purpose |
|----------|---------|---------|
| `ELODIN_ASSETS` | `./assets` | Directory for meshes, images, GLB files |
| `ELODIN_KDL_DIR` | `.` (cwd) | Directory for `.kdl` schematic files |
| `ELODIN_GPU` | `auto` | Nix shell GPU path (`nvidia` / `mesa` / `nvk`). Set **before** `nix develop`. |
| `ELODIN_GPU_PANIC` | unset | Set to `true` to force the GPU-not-found panic and print the `ELODIN_GPU=… nix develop` help. |
| `ELODIN_H264_ENCODER` | `auto` | Live `sensor_camera` `format="h264"`: `auto` tries NVENC / VideoToolbox then OpenH264; `cpu` forces OpenH264; any other value is an FFmpeg encoder name (`h264_nvenc`, …). `h264_vaapi` is not supported. |

## Cargo features

Optional features declared in `libs/elodin-editor/Cargo.toml` (re-exported by `apps/elodin/Cargo.toml`):

- `big_space` (default): upstream `big_space` 0.13 floating-origin layer.
- `inspector`: adds the `bevy-inspector-egui` runtime entity inspector.
- `debug`: enables `big_space`'s debug diagnostics.
- `tracy`: enables Tracy profiling (see `.cursor/skills/elodin-tracy/SKILL.md`).

Enable with `cargo run -p elodin --features "<list>" -- editor ...`.

## Source Layout

The editor is split across two crates:

### `libs/elodin-editor/` — Core library

```
src/
├── lib.rs              # Plugin registration, app setup
├── run.rs              # Main run loop and Bevy app builder
├── object_3d.rs        # 3D object spawning from KDL schematics
├── vector_arrow.rs     # Vector visualization (force/velocity arrows)
├── offset_parse.rs     # EQL viewport formula parsing
├── icon_rasterizer.rs  # Icon rendering
├── iter.rs             # Iterator utilities
├── ui/                 # Egui UI layer
│   ├── mod.rs          # Top-level UI orchestration
│   ├── tiles.rs        # Tiled panel layout system
│   ├── tiles/sidebar.rs
│   ├── inspector/      # Component inspector panels
│   │   ├── mod.rs
│   │   ├── entity.rs   # Entity property inspector
│   │   ├── viewport.rs # Viewport panel
│   │   ├── graph.rs    # Graph panel configuration
│   │   ├── dashboard.rs
│   │   ├── monitor.rs
│   │   ├── object3d.rs # 3D object inspector
│   │   └── ...
│   ├── plot/           # Telemetry graph rendering
│   │   ├── mod.rs
│   │   ├── data.rs     # Data fetching and buffering
│   │   ├── gpu.rs      # GPU-accelerated plot rendering
│   │   ├── widget.rs   # Egui plot widget
│   │   └── state.rs    # Plot state management
│   ├── plot_3d/        # 3D plot visualization
│   ├── schematic/      # KDL schematic loading and rendering
│   │   ├── mod.rs
│   │   ├── load.rs     # KDL file parsing
│   │   └── tree.rs     # Schematic tree view
│   ├── timeline/       # Playback timeline
│   │   ├── mod.rs
│   │   ├── timeline_controls.rs
│   │   └── timeline_slider.rs
│   ├── command_palette/ # Command palette (Ctrl+P)
│   ├── video_stream.rs  # Live video rendering
│   ├── theme.rs         # Color theme
│   ├── colors/          # Color system and presets
│   └── ...
└── plugins/            # Bevy plugins
    ├── mod.rs
    ├── view_cube/      # 3D orientation cube
    ├── camera_anchor/  # Camera tracking and anchoring
    ├── gizmos/         # Transform gizmos
    ├── navigation_gizmo/
    ├── editor_cam_touch/ # Touch input for camera
    ├── asset_cache/    # Asset caching
    ├── env_asset_source/ # ELODIN_ASSETS integration
    ├── web_asset/      # Web asset loading
    └── logical_key/    # Keyboard input handling
```

### `apps/elodin/` — CLI binary

The main entry point that ties together the editor with nox-py, s10, and Impeller2. Handles CLI argument parsing (`elodin editor`, `elodin run`, etc.).

## Key Subsystems

### Bevy Plugin Architecture

The editor registers as a set of Bevy plugins. Each feature area (view cube, camera, gizmos) is a self-contained plugin with its own components, systems, and resources. New features should follow this pattern.

### Egui UI Layer

The `ui/` module contains all immediate-mode UI rendering. Egui runs inside Bevy via `bevy_egui`. The tile-based layout system (`ui/tiles.rs`) manages panel arrangement (viewports, graphs, inspectors).

### Telemetry cache (SeriesStore)

**Strategy:** cache only what the UI uses. Full history per subscribed ID (no time-based SeriesStore GC). Menus use metadata/`EqlContext`, not store keys. Playback/scrub never wait on backfill — project whatever is already in RAM.

```text
DB ──► allowlisted GetTimeSeries backfill + live ──► TelemetryCache (SeriesStore)
                                                      ├─► project SelectedTimeRange → LineTree → GPU
                                                      └─► apply_cached_data @ playhead → 3D / inspectors
Metadata ──► EqlContext ──► ADD COMPONENT / palettes (full list)
```

| Piece | Where |
|-------|--------|
| Allowlist + reclaim | `ui/plot/data.rs` → `update_series_fetch_priority` |
| Backfill / live filter | `impeller2_bevy` → `backfill_cache`, `SeriesFetchPriority` |
| Schedule | editor + headless: priority then `backfill_cache` (`lib.rs`, `headless.rs`) |

**Allowlist** (`SeriesFetchPriority.high`): enabled graph lines; `Line3d` / `object_3d` EQL (including `thruster` intensity EQL for particle plumes); monitors; viewport `pos`/`look_at`/`up` EQL; `vector_arrow` EQL; path-registry adapter pairs (`*.world_pos`, …); sensor-camera `{entity}.world_pos`. Empty ⇒ no SeriesStore I/O. Leaving an ID drops it from RAM. **Any new live consumer must extend this allowlist** or it will be blank/stale. Adapter leaf match is case-sensitive (`WORLD_POS` ≠ `world_pos`).

**Plots:** LineTree is a visible-window projection only (sliding GC here ≠ SeriesStore). Tip/`LAST_*` fetches quantized (~100 ms) + prefetch margin; also immediate visible-window prefetch so tip fills before begin→end backfill. Do not clear a LineTree when the store has zero samples in-window (unless camera range moved). ≤30 s (`SHORT_WINDOW_ACCURACY_MICROS`): GPU `step = 1`, skip Hamann–Chen (`INDEX_BUFFER_LEN` = 131072 in `ui/plot/gpu.rs`, sized for ~4 kHz × 30 s). Longer windows: GPU stride on clip; CPU project stride only for &gt;10 min.

**Headless:** separate process, separate store — same priority + backfill or `sensor_view` poses freeze while effects still animate.

**Do not:** gate menus on SeriesStore keys; wait on `SeriesStoreLoadState.complete` for scrub; reintroduce full-metadata backfill; time-GC SeriesStore without fixing jump-to-start/scrub holes first.

### KDL Schematics

KDL files define 3D objects and viewport configurations. The loading pipeline:
1. `ui/schematic/load.rs` — Parses `.kdl` files
2. `object_3d.rs` — Spawns Bevy entities (meshes, GLB models, shapes)
3. `offset_parse.rs` — Evaluates EQL viewport formulas (rotate, translate)

### Video Streaming

`ui/video_stream.rs` handles live H.264/AV1 video decoding and rendering as textures in the 3D viewport. Uses the `video-toolbox` crate on macOS for hardware acceleration.

## Dependencies

Key crates used in the editor:

| Crate | Purpose |
|-------|---------|
| `bevy` | ECS game engine, 3D rendering, windowing |
| `bevy_egui` | Egui integration for Bevy |
| `egui` | Immediate-mode UI framework |
| `impeller2-bevy` | Bevy plugin for Impeller2 telemetry |
| `arrow` | Arrow data format for time-series |
| `eql` | Elodin Query Language parser |
| `nox` | Spatial math types |

## Development Tips

- Use `cargo watch` for fast iteration on UI changes
- The editor hot-reloads KDL schematics on file change
- Test with `examples/three-body/main.py` for a lightweight simulation
- GPU plot rendering is in `ui/plot/gpu.rs` — changes here affect all telemetry graphs
- The command palette (`ui/command_palette/`) is the entry point for user actions

## Screenshot-driven design, build, and test

Prefer Bevy's native window screenshot path over OS screen capture. It captures the full editor window (3D viewports **and** egui chrome) without macOS Screen Recording permissions, and works the same in local iteration and CI-style scripts.

### Harness

| Piece | Path / env |
|-------|------------|
| Plugin | `libs/elodin-editor/src/plugins/screenshot.rs` (`EnvScreenshotPlugin`) |
| Batch script | `scripts/ci/screenshot_examples.sh <out-dir> [example …]` |
| Activate | `ELODIN_SCREENSHOT=/abs/path/out.png` |
| Delay before capture | `ELODIN_SCREENSHOT_DELAY` (seconds; default **8**; use **12–20** for heavy examples) |
| Exit after write | `ELODIN_SCREENSHOT_EXIT=1` (required for bounded runs) |

The plugin queues `Screenshot::primary_window()`, waits until the PNG is non-empty on disk (async GPU readback), then sends `AppExit::Success` when exit is requested. Do **not** kill the process on a timer alone — that tears down the render thread mid-readback and yields a missing/empty PNG.

### One-shot capture (design / verify a change)

```bash
# Release binary is much faster for visual loops
cargo build -p elodin --release

rm -f /tmp/editor-shot.png
ELODIN_SCREENSHOT=/tmp/editor-shot.png \
ELODIN_SCREENSHOT_DELAY=12 \
ELODIN_SCREENSHOT_EXIT=1 \
  ./target/release/elodin editor examples/ball/main.py
```

Then **Read the PNG** in the agent (vision) and check concrete UI/scene facts — e.g. status-bar `RAM Usage: X.Y GB` (not `N/A` / not stuck at `0.0`), trajectory line present, view cube visible, graph panels populated. OCR (`tesseract`) is optional backup for status-bar text.

### Batch regression (examples gallery)

```bash
# Default set: ball three-body drone rc-jet apollo-lander video-stream
#             sensor-camera cube-sat voyager geo-frames
scripts/ci/screenshot_examples.sh /tmp/elodin-shots ball three-body drone
```

Env overrides: `ELODIN_BIN`, `ELODIN_SCREENSHOT_DELAY` (script default 20), `SCREENSHOT_WATCHDOG` (default 180). One editor at a time; each run has a watchdog so a hung capture cannot block forever.

### Workflow for UI / rendering changes

1. **Baseline** — screenshot the affected example(s) before the change into `/tmp/elodin-shots-baseline/`.
2. **Implement** — keep the change scoped; rebuild `elodin` (release for visual checks).
3. **Compare** — re-screenshot into `/tmp/elodin-shots-after/` and Read both PNGs. Assert the intended delta and that unrelated chrome (timeline, status bar, view cube) still looks healthy.
4. **Stress the failure mode** — if the bug was GPU/render-path specific (e.g. FPV + HDR + plot_3d), pick the example that exercises that path (`rc-jet`, `sensor-camera`, …), not only `ball`.
5. **Stale DBs** — if an example refuses to start with DB/time-travel errors, delete its on-disk DB (e.g. `rm -rf examples/voyager/dbs/voyager`, `rm -rf video-stream-db`) and retry. Do not dig into GStreamer until a clean DB still fails.

### Gotchas

- **Port 2240** — live `elodin editor` / `elodin run` binds the sim DB; do not parallelize with monte-carlo or another editor. Group-kill leftovers before the next case.
- **RAM gauge** — status-bar RSS is read via platform APIs in `ui/status_bar.rs` (not Bevy `SystemInformationDiagnosticsPlugin`). On macOS Bevy's sysinfo is built with `apple-app-store` and always reports 0 GiB for the current process.
- **video-stream** — clear `./video-stream-db` if the editor hangs or video never appears. `elodinsink` / `x264enc` / `srtsrc` come from `nix develop` or `nix develop .#run` (`GST_PLUGIN_PATH`). Do not prepend cargo `target/release` (`libelodin` makes `gst-plugin-scanner` abort the scan on GStreamer 1.26).
- **voyager** — needs SPICE kernels under `examples/voyager/nasa_spice_data/` and a clean DB dir after interrupted runs.
- **nix develop** — prefer it for CI-parity builds; for a tight screenshot loop, a warm `cargo build -p elodin --release` outside a full env rebuild is fine once the toolchain is already installed.

### When to use which example

| Goal | Example |
|------|---------|
| Lightest viewport + trail + vector label | `ball` |
| Multi-body + graph panels | `three-body` |
| GLB + joint animation | `drone` |
| FPV / plot_3d / aero | `rc-jet` |
| SITL + thrusters / descent | `apollo-lander` |
| H.264 tile in UI | `video-stream` |
| GPU sensor cameras / frusta | `sensor-camera` |
| Terrain / geo frames | `geo-frames` |
| Spacecraft + MEKF graphs | `cube-sat` |

## Key References

- Bevy Tips (ECS): [../bevy/SKILL.md](../bevy/SKILL.md)
- Editor QA plan: [../qa-test-plan/elodin-editor/test-plan.md](../qa-test-plan/elodin-editor/test-plan.md)
- Editor README: [apps/elodin/README.md](../../../apps/elodin/README.md)
- KDL schematic syntax: [docs/public/content/reference/schematic.md](../../../docs/public/content/reference/schematic.md)
- Command palette reference: [docs/public/content/reference/command-palette.md](../../../docs/public/content/reference/command-palette.md)

