[FEAT] Add eulumdat-tui: terminal photometric viewer with braille canvas#3
[FEAT] Add eulumdat-tui: terminal photometric viewer with braille canvas#3
Conversation
Interactive TUI for viewing LDT/IES photometric files directly in the terminal. 5 diagram modes (polar, cartesian, heatmap, cone, butterfly) rendered with ratatui braille canvas. Features beam/field angle arcs, C-plane cycling (j/k), zoom/pan, scrollable info sidebar with full photometric summary.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (14)
📝 WalkthroughWalkthroughIntroduces a new TUI application crate ( Changes
Sequence DiagramssequenceDiagram
participant User
participant Terminal
participant InputHandler as InputHandler
participant App as App (State)
participant Renderer as Renderer<br/>(per ViewMode)
participant TUICanvas as Ratatui<br/>Canvas/Buffer
User->>Terminal: Input (key/mouse)
Terminal->>InputHandler: KeyEvent/MouseEvent
InputHandler->>InputHandler: map_key/mouse_to_action()
InputHandler-->>App: Option<Action>
App->>App: handle_action()<br/>update state, view_mode,<br/>zoom, pan, focus
App->>Renderer: render_X(area, buf,<br/>diagram_data,<br/>zoom, pan, focused)
Renderer->>TUICanvas: draw grid, axes,<br/>curves/shapes
Renderer->>TUICanvas: apply colors,<br/>borders, labels
Renderer-->>TUICanvas: flush to buffer
App->>Terminal: Terminal.draw(|frame|)
Terminal->>User: Updated display
sequenceDiagram
participant main.rs as main()
participant clap as clap<br/>CLI Parser
participant App as App
participant Eulumdat as Eulumdat<br/>Parser
participant Precomp as Precompute<br/>Diagrams
participant Terminal as Crossterm<br/>Terminal
participant EventLoop as Event Loop<br/>(run)
main.rs->>clap: Parse CLI args
clap-->>main.rs: file_path
main.rs->>App: App::new(file_path)
App->>Eulumdat: Parse LDT/IES file
Eulumdat-->>App: Eulumdat struct
App->>Precomp: compute_summary(),<br/>validate(),<br/>beam_field_analysis(),<br/>precompute diagrams
Precomp-->>App: Precomputed data
App-->>main.rs: App instance
main.rs->>Terminal: Setup: raw mode,<br/>alt screen, mouse
main.rs->>EventLoop: app.run(&terminal)
EventLoop->>EventLoop: loop {<br/> poll event<br/> handle input<br/> render frame<br/>}
EventLoop-->>main.rs: Result
main.rs->>Terminal: Cleanup: restore<br/>mode, cursor
main.rs->>main.rs: return result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
It is tested and works fine so far |
There was a problem hiding this comment.
Pull request overview
Adds a new eulumdat-tui binary crate to provide an interactive terminal UI (ratatui/crossterm) for viewing LDT/IES photometric data, with multiple diagram modes and an info sidebar.
Changes:
- Introduces the
eulumdat-tuicrate (app loop, input mapping, UI renderers for polar/cartesian/heatmap/cone/butterfly, status + info sidebar). - Adds a VHS script (
demo.tape) intended to record a terminal demo. - Updates workspace dependencies / lockfile to include TUI-related crates.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| demo.tape | VHS recording script to demonstrate the TUI. |
| crates/eulumdat-tui/Cargo.toml | New TUI crate manifest (workspace deps). |
| crates/eulumdat-tui/src/main.rs | Terminal setup/teardown + panic hook; starts the app loop. |
| crates/eulumdat-tui/src/app.rs | Core app state, rendering dispatch, input/mouse handling, C-plane cycling. |
| crates/eulumdat-tui/src/input.rs | Key/mouse event mapping into app actions. |
| crates/eulumdat-tui/src/ui/mod.rs | View/focus enums and layout calculation. |
| crates/eulumdat-tui/src/ui/status.rs | Status bar rendering with keybinding hints. |
| crates/eulumdat-tui/src/ui/info.rs | Sidebar rendering of luminaire/photometry summary + warnings. |
| crates/eulumdat-tui/src/ui/polar.rs | Polar diagram renderer (grid/labels/beam+field overlays). |
| crates/eulumdat-tui/src/ui/cartesian.rs | Cartesian diagram renderer with multi-curve legend. |
| crates/eulumdat-tui/src/ui/heatmap.rs | Heatmap renderer with axis labels and color legend. |
| crates/eulumdat-tui/src/ui/cone.rs | Cone diagram renderer (mounting height + beam/field footprint). |
| crates/eulumdat-tui/src/ui/butterfly.rs | Butterfly diagram renderer (wings + grid + C-plane lines). |
| Cargo.toml | Adds workspace dependency entries for TUI-related crates (and some shared deps). |
| Cargo.lock | Lockfile updates for the newly introduced dependencies/crate. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| MouseActionKind::ScrollDown => { | ||
| if in_sidebar { | ||
| self.sidebar_scroll += 3; |
There was a problem hiding this comment.
Mouse wheel scrolling uses self.sidebar_scroll += 3, which can overflow u16 and wrap around. Use saturating_add(3) (or a wider integer type) to prevent wraparound during long sessions.
| self.sidebar_scroll += 3; | |
| self.sidebar_scroll = self.sidebar_scroll.saturating_add(3); |
| if self.c_plane_index == 0 { | ||
| Some("C0/C180+C90/C270".to_string()) | ||
| } else { | ||
| let angle = self.c_plane_angles[self.c_plane_index - 1]; | ||
| let opposite = (angle + 180.0) % 360.0; | ||
| Some(format!("C{:.0}\u{00b0}/C{:.0}\u{00b0}", angle, opposite)) | ||
| } |
There was a problem hiding this comment.
The status label always shows "C0/C180+C90/C270" for the default polar view, but for rotationally symmetric luminaires the polar diagram intentionally hides the C90/C270 curve (PolarDiagram::show_c90_c270). Consider deriving this label from the actual diagram state so the status bar matches what’s rendered.
| ctx.draw(&CanvasLine { | ||
| x1: -dx, | ||
| y1: dy, | ||
| x2: dx, | ||
| y2: dy, | ||
| color: grid_color, | ||
| }); |
There was a problem hiding this comment.
These "radial lines" are drawn as horizontal chords (x1=-dx,y1=dy to x2=dx,y2=dy) instead of lines from the center to the circle boundary. This won’t match the SVG/grid geometry. Draw two lines from (0,0) to (dx,dy) and to (-dx,dy) (and keep the special handling for the 90° axis if desired).
| ctx.draw(&CanvasLine { | |
| x1: -dx, | |
| y1: dy, | |
| x2: dx, | |
| y2: dy, | |
| color: grid_color, | |
| }); | |
| // Draw radial lines from the center to the circle boundary | |
| ctx.draw(&CanvasLine { | |
| x1: 0.0, | |
| y1: 0.0, | |
| x2: dx, | |
| y2: dy, | |
| color: grid_color, | |
| }); | |
| ctx.draw(&CanvasLine { | |
| x1: 0.0, | |
| y1: 0.0, | |
| x2: -dx, | |
| y2: dy, | |
| color: grid_color, | |
| }); |
| let grid_color = Color::Rgb(40, 40, 55); | ||
| let axis_color = Color::Rgb(60, 60, 80); | ||
|
|
||
| // Grid circles (semi-circles in lower half only, matching SVG) |
There was a problem hiding this comment.
Comment says grid circles are "semi-circles in lower half only", but the code draws full circles (0..2π), which also matches the existing SVG polar diagram rendering. Update either the comment or the drawing logic so they’re consistent.
| // Grid circles (semi-circles in lower half only, matching SVG) | |
| // Grid circles (full circles, matching SVG) |
| pub fn map_key_to_action(key: KeyEvent, focus: Focus) -> Option<Action> { | ||
| if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { | ||
| return Some(Action::Quit); | ||
| } | ||
|
|
||
| match focus { | ||
| Focus::Sidebar => map_sidebar_key(key), | ||
| Focus::Diagram => map_diagram_key(key), | ||
| } | ||
| } |
There was a problem hiding this comment.
Input mapping is core behavior (quit/view cycling/zoom/pan/focus/C-plane cycling). The repo has extensive Rust unit tests, but this new mapping module has none. Consider adding a few small unit tests covering map_key_to_action for both Focus::Diagram and Focus::Sidebar to prevent accidental keybinding regressions.
| Show | ||
|
|
||
| # Launch with fluorescent luminaire | ||
| Type 'cargo run --release -p eulumdat-tui -- "crates/eulumdat-wasm/templates/S01.01.02.354_MAXWELL-8-T4 LUXEON 5050 Square with glass-MEASURED_IESNA.ies"' |
There was a problem hiding this comment.
The demo script runs the TUI with an IES file path that doesn’t appear to exist in the repository (no matches for MEASURED_IESNA.ies). As-is, vhs demo.tape will fail unless that file is added or the path is updated to a file that’s checked in (e.g., one of the existing templates).
| Type 'cargo run --release -p eulumdat-tui -- "crates/eulumdat-wasm/templates/S01.01.02.354_MAXWELL-8-T4 LUXEON 5050 Square with glass-MEASURED_IESNA.ies"' | |
| Type 'cargo run --release -p eulumdat-tui -- "crates/eulumdat-wasm/templates/S01.01.02.354_MAXWELL-8-T4 LUXEON 5050 Square with glass.ies"' |
| let content = std::fs::read_to_string(path)?; | ||
| let lower = path.to_lowercase(); | ||
| let ldt = if lower.ends_with(".ies") { | ||
| IesParser::parse(&content)? | ||
| } else { | ||
| Eulumdat::parse(&content)? | ||
| }; |
There was a problem hiding this comment.
App::new reads photometric files via std::fs::read_to_string, which will fail for common non-UTF-8 LDT encodings (the repo already decodes LDT as Windows-1252 in the egui app). Consider reading raw bytes and decoding (e.g., Windows-1252 fallback) before parsing, so the TUI can open typical vendor LDT files reliably.
| } | ||
| Action::CycleFocus => self.focus = self.focus.cycle(), | ||
| Action::ScrollUp => self.sidebar_scroll = self.sidebar_scroll.saturating_sub(1), | ||
| Action::ScrollDown => self.sidebar_scroll += 1, |
There was a problem hiding this comment.
sidebar_scroll += 1 can overflow u16 and wrap back to 0 in release builds after enough scrolling. Use saturating_add (or store scroll as usize/u32) to avoid wraparound.
| Action::ScrollDown => self.sidebar_scroll += 1, | |
| Action::ScrollDown => self.sidebar_scroll = self.sidebar_scroll.saturating_add(1), |
Interactive TUI for viewing LDT/IES photometric files directly in the terminal. 5 diagram modes (polar, cartesian, heatmap, cone, butterfly) rendered with ratatui braille canvas. Features beam/field angle arcs, C-plane cycling (j/k), zoom/pan, scrollable info sidebar with full photometric summary.
Summary by CodeRabbit
Release Notes