| name | elodin-editor-dev |
| description | 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. |
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
cargo run --bin elodin editor examples/three-body/main.py
cargo watch --watch libs/elodin-editor \
-x 'run --bin elodin editor examples/three-body/main.py'
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. |
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.
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 >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:
ui/schematic/load.rs — Parses .kdl files
object_3d.rs — Spawns Bevy entities (meshes, GLB models, shapes)
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)
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)
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
- Baseline — screenshot the affected example(s) before the change into
/tmp/elodin-shots-baseline/.
- Implement — keep the change scoped; rebuild
elodin (release for visual checks).
- 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.
- 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.
- 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