930 lines
30 KiB
Markdown
930 lines
30 KiB
Markdown
# 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 G1–G4 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.
|