Skip to content

[FEAT] Add eulumdat-tui: terminal photometric viewer with braille canvas#3

Merged
holg merged 1 commit intomainfrom
eulumdat-tui
Mar 7, 2026
Merged

[FEAT] Add eulumdat-tui: terminal photometric viewer with braille canvas#3
holg merged 1 commit intomainfrom
eulumdat-tui

Conversation

@holg
Copy link
Copy Markdown
Owner

@holg holg commented Mar 7, 2026

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

  • New Features
    • Added a Terminal User Interface application for visualizing photometric diagrams from EULUMDAT files
    • Five diagram visualization modes: Polar, Cartesian, Heatmap, Cone, and Butterfly
    • Interactive navigation with keyboard and mouse controls (zoom, pan, view switching)
    • Information panel displaying luminaire specifications and photometric data
    • C-plane navigation for customizable polar diagram views

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.
Copilot AI review requested due to automatic review settings March 7, 2026 16:03
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 7, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0222883b-6846-45de-b9ac-8908efc81bb8

📥 Commits

Reviewing files that changed from the base of the PR and between 0f56a1e and a4d2fc9.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Cargo.toml
  • crates/eulumdat-tui/Cargo.toml
  • crates/eulumdat-tui/src/app.rs
  • crates/eulumdat-tui/src/input.rs
  • crates/eulumdat-tui/src/main.rs
  • crates/eulumdat-tui/src/ui/butterfly.rs
  • crates/eulumdat-tui/src/ui/cartesian.rs
  • crates/eulumdat-tui/src/ui/cone.rs
  • crates/eulumdat-tui/src/ui/heatmap.rs
  • crates/eulumdat-tui/src/ui/info.rs
  • crates/eulumdat-tui/src/ui/mod.rs
  • crates/eulumdat-tui/src/ui/polar.rs
  • crates/eulumdat-tui/src/ui/status.rs
  • demo.tape

📝 Walkthrough

Walkthrough

Introduces a new TUI application crate (eulumdat-tui) for interactive EULUMDAT visualization. Includes CLI argument parsing, terminal lifecycle management, a multi-view diagram system (polar, Cartesian, heatmap, cone, butterfly), keyboard/mouse input handling with focus switching, and zoom/pan controls. Workspace dependencies are expanded with Crossterm, Ratatui, and TOML support.

Changes

Cohort / File(s) Summary
Workspace Dependencies
Cargo.toml
Added workspace-level dependencies: toml (with parse/display features), rfd, crossterm, and ratatui for TUI support.
Crate Manifest
crates/eulumdat-tui/Cargo.toml
Created new crate manifest for eulumdat-tui with binary target and workspace-shared dependencies (anyhow, clap, crossterm, eulumdat, ratatui).
Application Entry Point
crates/eulumdat-tui/src/main.rs
CLI bootstrapper using clap::Parser; handles file argument, App initialization, terminal raw mode/alternate screen setup, panic hook for state restoration, and orderly cleanup after app.run().
Core Application Logic
crates/eulumdat-tui/src/app.rs
App struct encapsulating state (loaded data, precomputed diagrams, UI/view modes, interaction state); constructor loads LDT/IES files and precomputes photometric analysis; main loop orchestrates event handling (keyboard/mouse) and diagram rendering with pan/zoom/focus mechanics.
Input Handling
crates/eulumdat-tui/src/input.rs
Action and MouseAction enums with key-to-action mapping (global + per-focus key maps) and mouse event translation; supports scrolling, dragging, zoom, pan, view cycling, and C-plane navigation.
UI Framework
crates/eulumdat-tui/src/ui/mod.rs
Core UI module defining Focus (Sidebar/Diagram), ViewMode (Polar/Cartesian/Heatmap/Cone/Butterfly) enums with cycling logic, LayoutAreas struct, and calculate_layout() function for 3-pane layout (sidebar, diagram, status).
Diagram Renderers
crates/eulumdat-tui/src/ui/polar.rs, crates/eulumdat-tui/src/ui/cartesian.rs, crates/eulumdat-tui/src/ui/heatmap.rs, crates/eulumdat-tui/src/ui/cone.rs, crates/eulumdat-tui/src/ui/butterfly.rs
Specialized rendering functions for each diagram type; handle canvas setup, coordinate transforms, grid/axis drawing, data visualization with zoom/pan support, color theming, and focus-aware border styling.
Information & Status Panels
crates/eulumdat-tui/src/ui/info.rs, crates/eulumdat-tui/src/ui/status.rs
render_info() formats luminaire metadata, photometry, and validation warnings into a scrollable info panel; render_status() builds a dynamic status line with mode label, file name, keybindings, and mode-specific hints (C-plane for Polar).
Demo Script
demo.tape
VHS automation script demonstrating build, run, and interactive UI navigation (diagram mode cycling, zooming, focus switching, C-plane cycling) with screen recording output.

Sequence Diagrams

sequenceDiagram
    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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Whiskers twitching at this UI quest,
Five diagrams rendered to pass the test,
Polar curves and Cartesian grids align,
Zoom and pan make photometry shine!
From terminal's glow, bright visions take flight,
EULUMDAT dances in TUI delight!

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch eulumdat-tui

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@holg
Copy link
Copy Markdown
Owner Author

holg commented Mar 7, 2026

It is tested and works fine so far

@holg holg closed this Mar 7, 2026
@holg holg reopened this Mar 7, 2026
@holg holg merged commit 40c4903 into main Mar 7, 2026
8 of 9 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-tui crate (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;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
self.sidebar_scroll += 3;
self.sidebar_scroll = self.sidebar_scroll.saturating_add(3);

Copilot uses AI. Check for mistakes.
Comment on lines +216 to +222
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))
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +126 to +132
ctx.draw(&CanvasLine {
x1: -dx,
y1: dy,
x2: dx,
y2: dy,
color: grid_color,
});
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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,
});

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// Grid circles (semi-circles in lower half only, matching SVG)
// Grid circles (full circles, matching SVG)

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +32
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),
}
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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"'
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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"'

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +56
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)?
};
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}
Action::CycleFocus => self.focus = self.focus.cycle(),
Action::ScrollUp => self.sidebar_scroll = self.sidebar_scroll.saturating_sub(1),
Action::ScrollDown => self.sidebar_scroll += 1,
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
Action::ScrollDown => self.sidebar_scroll += 1,
Action::ScrollDown => self.sidebar_scroll = self.sidebar_scroll.saturating_add(1),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants