Files
openvistapro/docs/plans/phase-4-formats-scripts-ui.md
2026-05-20 16:04:04 -04:00

930 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 4+ Formats, Scripts, Paths, and UI Implementation Plan
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Expand OpenVistaPro from its current deterministic Rust CLI renderer into a clean-room terrain visualization pipeline with open-format importers, scriptable rendering, MakePath-style camera paths, and an eventual WGPU/egui application.
**Architecture:** Keep the existing single Rust crate until APIs stabilize. Add small modules around the current `HeightGrid`, `Scene`, `.ovp.toml` scene files, CLI, CPU top-down renderer, and CPU perspective demo renderer. Keep all import/compatibility code behind explicit module boundaries and optional features so the clean internal model stays independent from source formats.
**Tech Stack:** Rust 2024, cargo, clap, serde, toml, image, optional importer crates/features, future glam or nalgebra for path math, future wgpu/winit/egui for the app.
---
## Current baseline
The repository already has:
- `src/terrain.rs`: `HeightGrid` with validated dimensions, row-major samples, deterministic `plane` and `radial_hill` fixtures.
- `src/scene.rs`: serializable camera, light, water, tree-line, snow-line, and haze settings.
- `src/scene_file.rs`: project-owned `.ovp.toml` files with `schema = "openvistapro.scene"`, `version = 1`, and a serialized `Scene` payload.
- `src/colormap.rs`: deterministic elevation-band colors.
- `src/render.rs`: deterministic top-down PNG renderer plus spike-quality CPU perspective raymarcher.
- `src/import.rs`: open-format import boundary with `ovp-text`, feature-gated SRTM/HGT bytes, feature-gated ESRI ASCII Grid parsing, and feature-gated GeoTIFF parsing.
- `src/script.rs` and `src/script_exec.rs`: project-owned script parsing and execution for presets, grayscale PNG heightmaps, threshold changes, and PNG render outputs.
- `src/path.rs`: deterministic MakePath-inspired camera keyframe interpolation.
- `src/app_state.rs`, `src/app.rs`, and `src/bin/openvistapro_app.rs`: feature-gated `app` shell using `eframe`/`egui` with scene controls and CPU preview rendering.
- `src/cli.rs`: `info`, `scene export`, `render`, and `script run` commands.
- `README.md`, `docs/legal/asset-policy.md`, `docs/research/reference-inventory.md`, and `docs/knowledgebase/*.md`: clean-room context and project constraints.
Phase 4 thin slices have landed as implementation checkpoints, while this file remains a historical implementation plan for follow-on work and validation.
## Non-negotiable clean-room and repository hygiene rules
- Do not read, copy, decompile, translate, track, or commit proprietary VistaPro binaries, installers, archives, ISO/7z files, screenshots, extracted program files, sample landscapes, manuals, or scratch outputs.
- Keep `reference/`, `.work/`, and `target/` local-only and ignored.
- Use open/public formats and synthetic fixtures for tests.
- Commit only original source, original documentation, tiny synthetic fixtures, and openly licensed data with attribution.
- Treat VistaPro and MakePath as workflow inspiration. Do not attempt historical file compatibility until a later, separately reviewed clean-room plan approves the scope.
- Prefer TDD for every code change: write a focused failing test, run it and confirm the expected failure, implement the smallest code, then run the targeted test and the full validation suite.
## Dependency map and ordering
1. Importer foundation must come first: module shape, metadata, and tiny synthetic fixtures.
2. Add one importer at a time, starting with the smallest pure-Rust parser. HGT/SRTM is the best first real open-format target because the file layout is simple.
3. Add importer CLI plumbing after at least one importer can return `HeightGrid` plus source metadata.
4. Evolve scene/project metadata only after imported terrain needs provenance, source units, or vertical scale.
5. Add the script MVP after CLI render/import commands are stable, because scripts should drive existing commands instead of inventing parallel behavior.
6. Add path generation after script commands can carry camera keyframes.
7. Add WGPU/egui only after the CPU renderer, scene files, importers, script execution, and path generation have stable tests and sample commands.
## Milestone A: Importer foundation
### Task A1: Create an importer module skeleton
**Objective:** Define a stable place for open terrain importers without changing renderer behavior.
**Files:**
- Create: `src/import.rs` or `src/import/mod.rs`
- Modify: `src/lib.rs`
- Test: unit tests inside `src/import.rs` or `src/import/mod.rs`
**Step 1: Write failing test**
Add a test asserting that a `TerrainSourceMetadata` value records source format, dimensions, and elevation units.
Expected shape:
```rust
#[test]
fn metadata_records_format_dimensions_and_units() {
let meta = TerrainSourceMetadata::new("hgt", 1201, 1201, ElevationUnit::Meters);
assert_eq!(meta.format(), "hgt");
assert_eq!(meta.width(), 1201);
assert_eq!(meta.height(), 1201);
assert_eq!(meta.elevation_unit(), ElevationUnit::Meters);
}
```
**Step 2: Verify RED**
Run: `cargo test import::tests::metadata_records_format_dimensions_and_units`
Expected: FAIL because `import` and its metadata types do not exist.
**Step 3: Implement minimal code**
Create the module, expose it from `src/lib.rs`, and add only the metadata types and accessors required by the test.
**Step 4: Verify GREEN**
Run: `cargo test import`
Expected: PASS.
**Step 5: Full validation**
Run: `cargo fmt --check && cargo test && cargo clippy --all-targets -- -D warnings`
### Task A2: Define importer result and error boundaries
**Objective:** Return both a `HeightGrid` and source metadata while keeping parser errors typed and user-displayable.
**Files:**
- Modify: `src/import.rs` or `src/import/mod.rs`
- Test: unit tests in the same module
**Step 1: Write failing tests**
Add tests for:
- `ImportedTerrain::new(grid, metadata)` exposes the grid and metadata.
- `ImportError::InvalidDimensions` renders a helpful display message.
**Step 2: Verify RED**
Run: `cargo test import::tests::imported_terrain_exposes_grid_and_metadata import::tests::invalid_dimensions_error_is_displayable`
Expected: FAIL because the types do not exist yet.
**Step 3: Implement minimal code**
Add `ImportedTerrain`, `ImportError`, `Display`, and `Error` impls. Do not add a parser yet.
**Step 4: Verify GREEN**
Run: `cargo test import`
Expected: PASS.
### Task A3: Advertise planned importers without claiming support
**Objective:** Keep `openvistapro info` honest while pointing users to planned importer work.
**Files:**
- Modify: `src/cli.rs`
- Test: unit tests inside `src/cli.rs`
**Step 1: Write failing test**
Add a test that `info_text()` contains a separate planned/importer-roadmap line or docs pointer while `supported_importers()` remains empty until the first parser lands.
**Step 2: Verify RED**
Run: `cargo test cli::tests::info_text_mentions_importer_roadmap_without_enabling_importers`
Expected: FAIL because no importer roadmap text exists.
**Step 3: Implement minimal code**
Update `info_text()` only. Do not list DEM/HGT/GeoTIFF as supported until real parsers exist.
**Step 4: Verify GREEN**
Run: `cargo test cli::tests::info_text_mentions_importer_roadmap_without_enabling_importers`
Expected: PASS.
## Milestone B: First open terrain importer: HGT/SRTM
### Task B1: Add tiny HGT parser from bytes
**Objective:** Parse a square HGT/SRTM-style big-endian signed 16-bit elevation grid into `HeightGrid` using synthetic bytes.
**Files:**
- Create or modify: `src/import/hgt.rs` if using a directory module, otherwise add `hgt` submodule in `src/import.rs`
- Modify: `src/import/mod.rs` or `src/import.rs`
- Test: unit tests for the HGT parser
**Step 1: Write failing test**
Use a tiny synthetic 3x3 byte buffer. Do not use real downloaded HGT files yet.
```rust
#[test]
fn parses_tiny_hgt_big_endian_i16_grid() {
let bytes = [
0x00, 0x01, 0x00, 0x02, 0x00, 0x03,
0x00, 0x04, 0x00, 0x05, 0x00, 0x06,
0x00, 0x07, 0x00, 0x08, 0x00, 0x09,
];
let imported = parse_hgt_bytes(&bytes, 3).expect("3x3 HGT bytes should parse");
assert_eq!(imported.grid().width(), 3);
assert_eq!(imported.grid().height(), 3);
assert_eq!(imported.grid().sample(0, 0), Some(1.0));
assert_eq!(imported.grid().sample(2, 2), Some(9.0));
}
```
**Step 2: Verify RED**
Run: `cargo test hgt::tests::parses_tiny_hgt_big_endian_i16_grid`
Expected: FAIL because the parser is missing.
**Step 3: Implement minimal code**
Read pairs with `i16::from_be_bytes`, convert to `f32`, and reject buffers whose length is not `side * side * 2`.
**Step 4: Verify GREEN**
Run: `cargo test hgt`
Expected: PASS.
### Task B2: Reject malformed HGT buffers
**Objective:** Prevent silent truncation or wrong dimensions.
**Files:**
- Modify: HGT parser module
- Test: HGT unit tests
**Step 1: Write failing tests**
Add tests for:
- Odd byte count.
- Declared side length whose sample count does not match the byte count.
- Zero side length.
**Step 2: Verify RED**
Run: `cargo test hgt::tests::rejects_malformed_hgt_buffers`
Expected: FAIL for missing validation.
**Step 3: Implement minimal code**
Map validation failures to `ImportError` variants.
**Step 4: Verify GREEN**
Run: `cargo test hgt`
Expected: PASS.
### Task B3: Add CLI import path for HGT to render
**Objective:** Let the user render a synthetic or local HGT file through the existing render pipeline.
**Files:**
- Modify: `src/cli.rs`
- Modify: importer module
- Test: `src/cli.rs` tests plus importer tests
**Step 1: Write failing parser test**
Add a CLI parsing test for a future command shape, for example:
```text
openvistapro render --import-hgt /tmp/tiny.hgt --hgt-side 3 --output /tmp/out.png
```
Keep `--preset` mutually exclusive with importer input once implemented.
**Step 2: Verify RED**
Run: `cargo test cli::tests::parses_render_with_hgt_import`
Expected: FAIL because the flags do not exist.
**Step 3: Implement minimal code**
Add optional render args for HGT input and side length. Build the `HeightGrid` from HGT when present; otherwise use the existing preset path.
**Step 4: Verify GREEN**
Run: `cargo test cli::tests::parses_render_with_hgt_import cargo test hgt`
Expected: PASS.
**Feature-specific sample command**
Run after creating a tiny synthetic fixture in `/tmp`:
```bash
cargo run -- render --import-hgt /tmp/openvistapro-tiny.hgt --hgt-side 3 --output /tmp/openvistapro-hgt.png
```
Expected: PNG is created and has 3x3 dimensions unless explicit render scaling is added later.
## Milestone C: DEM and GeoTIFF planning and feature gates
### Task C1: Add importer feature flags without pulling heavy dependencies by default
**Objective:** Make optional importer capabilities visible and keep the default build lightweight.
**Files:**
- Modify: `Cargo.toml`
- Modify: `src/import.rs` or `src/import/mod.rs`
- Test: compile-time cfg tests where practical
**Step 1: Write failing check**
Add a cfg-gated unit test or compile guard showing `dem` and `geotiff` modules are not required in the default build.
**Step 2: Verify RED**
Run: `cargo test --no-default-features`
Expected: FAIL only until features are declared and module cfgs compile.
**Step 3: Implement minimal code**
Add feature entries such as:
```toml
[features]
default = []
import-dem = []
import-hgt = []
import-geotiff = []
```
Only add external dependencies when a parser needs them.
**Step 4: Verify GREEN**
Run: `cargo test --no-default-features && cargo test --all-features`
Expected: PASS.
### Task C2: Add ASCII DEM parser MVP
**Objective:** Support a simple open DEM text fixture before attempting broad real-world DEM variants.
**Files:**
- Create: `src/import/dem.rs`
- Modify: `src/import/mod.rs`
- Test: DEM unit tests with tiny inline text fixtures
**Step 1: Write failing test**
Use a tiny project-owned text fixture that declares width, height, units, and samples. Avoid proprietary files.
**Step 2: Verify RED**
Run: `cargo test dem::tests::parses_tiny_ascii_dem_fixture --features import-dem`
Expected: FAIL because parser is missing.
**Step 3: Implement minimal code**
Parse only the documented tiny subset. Reject unknown or incomplete input. Document that full DEM dialect support is future work.
**Step 4: Verify GREEN**
Run: `cargo test dem --features import-dem`
Expected: PASS.
### Task C3: GeoTIFF importer behind an optional feature (implemented)
**Status:** Done. Landed as the optional `import-geotiff` feature; this section
records the implemented design. The crate survey behind it is
[`docs/research/geotiff-import-strategy.md`](../research/geotiff-import-strategy.md).
**Objective:** Parse single-band GeoTIFF elevation tiles into the internal
`HeightGrid` without making default builds fragile or pulling a native
dependency.
**Outcome:** A pure-Rust path was chosen over GDAL. The importer uses the
`geotiff-reader` crate (`local` feature, `cog`/network feature off), declared as
an optional dependency gated by the `import-geotiff` Cargo feature. There is no
GDAL dependency and no native toolchain requirement, so `import-geotiff` builds
and tests run anywhere `cargo` runs — the "documented skip if native GDAL is not
installed" escape hatch is not needed and is reserved for any future,
separately reviewed GDAL-backed feature.
**Files:**
- `src/import/geotiff.rs`: `cfg(feature = "import-geotiff")` module exposing
`parse_geotiff_bytes(&[u8]) -> Result<ImportedTerrain, ImportError>`. It reads
the payload in memory, rejects non-single-band rasters, decodes the raster as
`f32`, and builds a `HeightGrid` plus `TerrainSourceMetadata` (format
`"geotiff"`). Reader/decode failures map to `ImportError::MalformedSource`.
- `src/import.rs`: declares the `geotiff` submodule under the feature cfg.
- `Cargo.toml`: `import-geotiff = ["dep:geotiff-reader"]`; `geotiff-writer` and
`ndarray` are dev-dependencies used only to generate the test fixture.
- `src/cli.rs`: `supported_importers()` and `info_text()` list `geotiff` only
when the feature is built. (A `render --import-geotiff` flag mirroring the HGT
path remains future work.)
**Fixture hygiene:** Tests do not commit any GeoTIFF binary. They generate a
tiny single-band elevation tile in memory with `geotiff-writer` (dev-dependency)
and parse it back, mirroring the synthetic-fixture approach used for HGT. No
proprietary data and no large real-world DEMs enter the repository; real tiles
may be used only for local manual verification under the already-ignored
`reference/` and `.work/` directories.
**Validation**
Run: `cargo test --no-default-features`, `cargo test geotiff --features import-geotiff`,
and `cargo test --all-features`.
Expected: default and `--no-default-features` builds stay GeoTIFF-free; the
feature build and `--all-features` exercise the parser and pass without any
native dependency.
## Milestone D: Scene/project metadata evolution
### Task D1: Add terrain source metadata to project-owned scene files
**Objective:** Preserve source provenance and vertical units without putting importer-specific details into the renderer.
**Files:**
- Modify: `src/scene.rs`
- Modify: `src/scene_file.rs`
- Test: scene and scene_file unit tests
**Step 1: Write failing tests**
Add tests that:
- Default scenes have no terrain source metadata.
- Scene file round-trips a scene with source format, original dimensions, elevation unit, and optional path display string.
- Version handling remains explicit.
**Step 2: Verify RED**
Run: `cargo test scene_file::tests::round_trips_terrain_source_metadata`
Expected: FAIL because scene metadata does not exist.
**Step 3: Implement minimal code**
Add serializable structs and keep fields optional. Decide whether this is backward-compatible under `SCENE_VERSION = 1` or whether to bump to version 2 with an upgrade path.
**Step 4: Verify GREEN**
Run: `cargo test scene scene_file`
Expected: PASS.
### Task D2: Add project file plan before implementing a new container
**Objective:** Decide whether `.ovp.toml` remains a scene-only file or grows into a project document that references terrain/import metadata.
**Files:**
- Create: `docs/plans/phase-4-project-file.md` if needed
- No production code in this task unless the plan chooses the format
**Verification:**
Run: `git diff --check`
Expected: no whitespace errors.
## Milestone E: Script language MVP
### Task E1: Define a tiny project-owned script command model
**Objective:** Represent a script as commands over existing scene/render operations without emulating proprietary syntax.
**Files:**
- Create: `src/script.rs`
- Modify: `src/lib.rs`
- Test: unit tests inside `src/script.rs`
**Step 1: Write failing test**
Start with a deliberately small, original syntax such as:
```text
set camera.position 0 30 -20
set camera.target 16 3 16
render frame 0
```
Add a test that parses those lines into typed commands.
**Step 2: Verify RED**
Run: `cargo test script::tests::parses_set_camera_and_render_frame_commands`
Expected: FAIL because `script` does not exist.
**Step 3: Implement minimal code**
Add `Script`, `ScriptCommand`, parser errors, and a line-oriented parser. Do not execute commands yet.
**Step 4: Verify GREEN**
Run: `cargo test script`
Expected: PASS.
### Task E2: Execute script scene mutations without rendering
**Objective:** Apply `set` commands to `Scene` so script behavior can be tested without image output.
**Files:**
- Modify: `src/script.rs`
- Test: script unit tests
**Step 1: Write failing test**
Assert that applying parsed camera and haze commands changes a `Scene` exactly.
**Step 2: Verify RED**
Run: `cargo test script::tests::applies_scene_mutation_commands`
Expected: FAIL because execution is missing.
**Step 3: Implement minimal code**
Add an executor that handles only scene mutations. Unsupported commands return typed errors.
**Step 4: Verify GREEN**
Run: `cargo test script`
Expected: PASS.
### Task E3: Add CLI script dry-run
**Objective:** Let users validate script parsing and scene mutations before rendering frames.
**Files:**
- Modify: `src/cli.rs`
- Modify: `src/script.rs`
- Test: CLI parser tests and script tests
**Step 1: Write failing CLI test**
Target command:
```text
openvistapro script check --input /tmp/example.ovpscript
```
**Step 2: Verify RED**
Run: `cargo test cli::tests::parses_script_check_command`
Expected: FAIL because command is missing.
**Step 3: Implement minimal code**
Add `script check` to parse and report command count. No rendering yet.
**Step 4: Verify GREEN**
Run: `cargo test cli::tests::parses_script_check_command cargo test script`
Expected: PASS.
**Feature-specific sample command**
```bash
cargo run -- script check --input /tmp/openvistapro-example.ovpscript
```
Expected: prints a parse summary and exits successfully for valid script text.
### Task E4: Render a scripted single frame
**Objective:** Let a script produce one PNG through the existing CPU renderer.
**Files:**
- Modify: `src/cli.rs`
- Modify: `src/script.rs`
- Test: CLI/script tests
**Step 1: Write failing test**
Add a test that a script with one `render frame 0` command produces a PNG at a requested output directory using a small synthetic `HeightGrid`.
**Step 2: Verify RED**
Run: `cargo test script::tests::renders_single_scripted_frame`
Expected: FAIL because render execution is missing.
**Step 3: Implement minimal code**
Use existing `render_top_down_to_path` or `render_perspective_to_path`. Keep camera-demo/perspective mode explicit.
**Step 4: Verify GREEN**
Run: `cargo test script cli`
Expected: PASS.
**Feature-specific sample command**
```bash
cargo run -- script render --input /tmp/openvistapro-example.ovpscript --preset hill --output-dir /tmp/openvistapro-frames
```
Expected: creates `frame-0000.png` for the MVP.
## Milestone F: MakePath-style camera path generator
### Task F1: Add camera keyframe data structures
**Objective:** Represent camera/target path control points in project-owned types.
**Files:**
- Create: `src/path.rs`
- Modify: `src/lib.rs`
- Test: unit tests inside `src/path.rs`
**Step 1: Write failing test**
Assert that a path with two keyframes exposes start/end camera positions and frame numbers.
**Step 2: Verify RED**
Run: `cargo test path::tests::camera_path_records_keyframes_in_order`
Expected: FAIL because path types do not exist.
**Step 3: Implement minimal code**
Add `CameraKeyframe` and `CameraPath`. Validate monotonically increasing frame numbers.
**Step 4: Verify GREEN**
Run: `cargo test path`
Expected: PASS.
### Task F2: Add linear interpolation before splines
**Objective:** Generate deterministic per-frame camera poses from two keyframes.
**Files:**
- Modify: `src/path.rs`
- Test: path unit tests
**Step 1: Write failing test**
Assert frame 5 between frame 0 and frame 10 is the midpoint for position, target, and FOV.
**Step 2: Verify RED**
Run: `cargo test path::tests::linear_interpolation_returns_midpoint_pose`
Expected: FAIL because interpolation is missing.
**Step 3: Implement minimal code**
Add linear interpolation only. Avoid splines until linear output is tested.
**Step 4: Verify GREEN**
Run: `cargo test path`
Expected: PASS.
### Task F3: Add spline interpolation as a separate, tested behavior
**Objective:** Add MakePath-inspired smooth camera paths without copying its implementation or file formats.
**Files:**
- Modify: `src/path.rs`
- Test: path unit tests
**Step 1: Write failing tests**
Use project-owned numeric fixtures for Catmull-Rom or cubic interpolation:
- Interpolation passes through keyframes.
- Endpoints do not overshoot for a simple straight path.
- Output is deterministic.
**Step 2: Verify RED**
Run: `cargo test path::tests::spline_path_passes_through_keyframes`
Expected: FAIL because spline mode is missing.
**Step 3: Implement minimal code**
Implement a standard documented spline algorithm from first principles. Link to the algorithm reference in comments if useful; do not use MakePath code or data.
**Step 4: Verify GREEN**
Run: `cargo test path`
Expected: PASS.
### Task F4: Export generated paths to the OpenVistaPro script MVP
**Objective:** Let the path generator create a script that the script renderer already understands.
**Files:**
- Modify: `src/path.rs`
- Modify: `src/script.rs`
- Modify: `src/cli.rs`
- Test: path/script/CLI tests
**Step 1: Write failing test**
Assert that a two-keyframe path exported for 3 frames produces 3 camera set commands and 3 render commands.
**Step 2: Verify RED**
Run: `cargo test path::tests::exports_path_as_script_commands`
Expected: FAIL because export is missing.
**Step 3: Implement minimal code**
Add a converter from path samples to `ScriptCommand`s.
**Step 4: Verify GREEN**
Run: `cargo test path script cli`
Expected: PASS.
**Feature-specific sample command**
```bash
cargo run -- path generate --input /tmp/openvistapro-path.ovppath --frames 60 --output /tmp/openvistapro-path.ovpscript
cargo run -- script check --input /tmp/openvistapro-path.ovpscript
```
Expected: generated script parses successfully.
## Milestone G: WGPU/egui application after CLI stability
**Status:** Tasks G1G4 have landed. `src/app_state.rs` holds testable app state and `AppAction` reducers; `src/app.rs` and `src/bin/openvistapro_app.rs` provide the `app`-feature `eframe`/`egui` shell. The shell now docks terrain, scene/camera, render, import, script, path, and project controls around the CPU top-down/perspective preview, with a bottom status bar and backend-backed import/script/path actions.
The shell map is tracked in [`docs/knowledgebase/ui-panel-map.md`](../knowledgebase/ui-panel-map.md).
Remaining UI roadmap: Task G5 (WGPU renderer backend) plus the still-open gaps — legacy menus/dialogs, richer file/project chrome, animation-frame export, deeper script/path editors, and palette import/export / texture loading. All of it stays clean-room: no proprietary VistaPro assets, menus, or screenshots enter the repository.
### Task G1: Create app-state crate/module without a window
**Objective:** Separate UI state from rendering and file formats before adding WGPU.
**Files:**
- Create: `src/app_state.rs`
- Modify: `src/lib.rs`
- Test: unit tests inside `src/app_state.rs`
**Step 1: Write failing test**
Assert default app state contains a `Scene`, a synthetic terrain preset selection, and no loaded source file.
**Step 2: Verify RED**
Run: `cargo test app_state::tests::default_app_state_has_scene_and_no_loaded_file`
Expected: FAIL because app state does not exist.
**Step 3: Implement minimal code**
Add pure data structures only. Do not add WGPU or egui yet.
**Step 4: Verify GREEN**
Run: `cargo test app_state`
Expected: PASS.
### Task G2: Add egui controls as pure state mutations first
**Objective:** Make UI semantics testable before creating a native window.
**Files:**
- Modify: `src/app_state.rs`
- Test: unit tests in `src/app_state.rs`
**Step 1: Write failing tests**
Add tests for operations like setting water level, moving camera target, toggling renderer mode, and selecting an importer source.
**Step 2: Verify RED**
Run: `cargo test app_state::tests::updates_scene_controls_from_ui_actions`
Expected: FAIL because action handling is missing.
**Step 3: Implement minimal code**
Add an `AppAction` enum and reducer-style `apply` method.
**Step 4: Verify GREEN**
Run: `cargo test app_state`
Expected: PASS.
### Task G3: Add native app feature gate
**Objective:** Keep CLI builds fast while making room for WGPU/winit/egui.
**Files:**
- Modify: `Cargo.toml`
- Create: `src/bin/openvistapro_app.rs` or a gated module
- Test: compile checks
**Step 1: Write failing check**
Add a feature-gated binary or module that should compile only with `--features app`.
**Step 2: Verify RED**
Run: `cargo check --features app`
Expected: FAIL until dependencies/module stubs exist.
**Step 3: Implement minimal code**
Add a window stub with no GPU terrain rendering. It may display app title and basic scene values.
**Step 4: Verify GREEN**
Run: `cargo check --features app && cargo test --all-features`
Expected: PASS.
### Task G4: Bridge CPU preview into the app
**Objective:** Display the existing deterministic CPU top-down preview in egui before adding GPU terrain rendering.
**Files:**
- Modify: app module/binary
- Modify: `src/render.rs` only if a reusable image buffer helper is needed
- Test: app-state/render tests where possible
**Step 1: Write failing test**
Add a pure test that app state can request a preview image and receives dimensions matching the current terrain.
**Step 2: Verify RED**
Run: `cargo test app_state::tests::preview_request_returns_expected_dimensions --features app`
Expected: FAIL until preview bridge exists.
**Step 3: Implement minimal code**
Reuse `render_top_down` and upload/display the resulting image in the UI. Avoid duplicating renderer logic.
**Step 4: Verify GREEN**
Run: `cargo test --features app`
Expected: PASS.
### Task G5: Add WGPU renderer only after CPU preview is usable
**Objective:** Introduce GPU rendering as a second renderer backend, not as a replacement for the tested CPU reference.
**Files:**
- Create: `src/gpu_renderer.rs` or `src/render/gpu.rs` after deciding whether to split `src/render.rs`
- Modify: app module/binary
- Test: compile checks and pure math tests
**Step 1: Write failing checks**
Start with tests for camera matrices, terrain buffer dimensions, and shader-uniform construction. Avoid screenshot tests at first.
**Step 2: Verify RED**
Run: `cargo test gpu_renderer --features app`
Expected: FAIL because GPU renderer types do not exist.
**Step 3: Implement minimal code**
Add resource creation and a flat/grid terrain draw path. Keep renderer API driven by `HeightGrid` and `Scene`.
**Step 4: Verify GREEN**
Run: `cargo test --features app && cargo check --features app`
Expected: PASS.
## Cross-milestone validation commands
Run these before every focused commit:
```bash
cargo fmt --check
cargo test
git diff --check
```
Run these before pushing any code-changing branch:
```bash
cargo fmt --check
cargo test
cargo clippy --all-targets -- -D warnings
git diff --check
```
Run feature-specific checks when a milestone enables optional code:
```bash
cargo test --no-default-features
cargo test --all-features
cargo check --features app
cargo test --features import-dem
cargo test --features import-geotiff
```
Use sample commands as acceptance checks when the related CLI surface exists:
```bash
cargo run -- info
cargo run -- scene export --output /tmp/openvistapro-default.ovp.toml
cargo run -- render --preset hill --width 256 --height 256 --output /tmp/openvistapro-hill.png
cargo run -- render --preset hill --scene /tmp/openvistapro-default.ovp.toml --width 256 --height 256 --output /tmp/openvistapro-hill-from-scene.png
cargo run -- render --preset hill --camera-demo --width 256 --height 192 --output /tmp/openvistapro-perspective.png
```
## Commit strategy
- Commit the plan itself as `docs: expand phase 4 implementation plan`.
- Future implementation branches should commit one RED/GREEN slice at a time.
- Keep importer fixture commits separate from parser commits when licenses or provenance need review.
- Do not push generated PNGs, local HGT/DEM/GeoTIFF samples, archives, or scratch directories.
## Definition of done for Phase 4+
Phase 4 is not one large task. It is complete when:
1. At least one open terrain importer can create a `HeightGrid` from a tested, legal fixture.
2. `openvistapro info` reports real importer support accurately.
3. `.ovp.toml` scene/project metadata can preserve terrain provenance where needed.
4. A project-owned script MVP can check scripts and render at least one deterministic frame.
5. A path generator can produce scriptable camera frames from project-owned keyframes.
6. The interactive app has tested app state and can show a CPU preview before WGPU terrain rendering is attempted.
7. Default builds remain clean: `cargo fmt --check`, `cargo test`, `cargo clippy --all-targets -- -D warnings`, and `git diff --check` pass.