feat: wire vertical exaggeration through the shell #13

Merged
moldybits merged 25 commits from feat/terrain-gen-abstraction into main 2026-05-20 23:00:29 -04:00
6 changed files with 78 additions and 107 deletions
Showing only changes of commit 450b950762 - Show all commits
+2
View File
@@ -25,6 +25,7 @@ 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 --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 --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 cargo run -- render --preset hill --camera-demo --width 256 --height 192 --output /tmp/openvistapro-perspective.png
cargo run -- render --preset hill --quality final --output /tmp/openvistapro-renders/
cargo run --features app --bin openvistapro_app cargo run --features app --bin openvistapro_app
``` ```
@@ -57,6 +58,7 @@ cargo test --all-features
``` ```
The default `render` mode writes a deterministic top-down elevation preview. The default `render` mode writes a deterministic top-down elevation preview.
Passing `--quality preview|balanced|final` selects the project-owned quality profile; if `--output` points to an existing directory, the renderer derives a quality-specific PNG name there.
Passing `--camera-demo` switches to the current CPU perspective renderer spike: Passing `--camera-demo` switches to the current CPU perspective renderer spike:
a simple pinhole-camera raymarcher with bilinear height sampling, fixed step a simple pinhole-camera raymarcher with bilinear height sampling, fixed step
size, sky gradient, and distance haze. It is intended as a readable reference size, sky gradient, and distance haze. It is intended as a readable reference
+2 -2
View File
@@ -29,7 +29,7 @@ Notes:
| Vertical exaggeration | VistaPro manuals describe vertical scaling / scene exaggeration controls. | Implemented | `src/scene.rs` (`Scene.vertical_exaggeration`), `src/app.rs`, `src/app_state.rs`, `src/render.rs`, tests in `src/render.rs`, and scene-file round-trips in `src/scene_file.rs`. | The basic scene-level exaggeration slice is now wired through the shell, renderer, and scene files; future work can explore per-tool or legacy-style exaggeration workflows. | | Vertical exaggeration | VistaPro manuals describe vertical scaling / scene exaggeration controls. | Implemented | `src/scene.rs` (`Scene.vertical_exaggeration`), `src/app.rs`, `src/app_state.rs`, `src/render.rs`, tests in `src/render.rs`, and scene-file round-trips in `src/scene_file.rs`. | The basic scene-level exaggeration slice is now wired through the shell, renderer, and scene files; future work can explore per-tool or legacy-style exaggeration workflows. |
| Color maps / palettes / texture image loading | VistaPro 3 manual includes loading PCX images, adding texture, and saving/loading color maps. | Implemented | `src/scene.rs` (`Palette` thresholds/bands + colors), `src/app.rs` color-map editor, `src/colormap.rs`, `src/render.rs`, `src/script_exec.rs`, `src/scene_file.rs`. | Palette import/export and PCX/texture loading remain future clean-room work. | | Color maps / palettes / texture image loading | VistaPro 3 manual includes loading PCX images, adding texture, and saving/loading color maps. | Implemented | `src/scene.rs` (`Palette` thresholds/bands + colors), `src/app.rs` color-map editor, `src/colormap.rs`, `src/render.rs`, `src/script_exec.rs`, `src/scene_file.rs`. | Palette import/export and PCX/texture loading remain future clean-room work. |
| Preview / final render workflow | VistaPro manuals describe rough preview rendering and full render output. | Implemented | `src/render.rs` (`render_top_down`, `render_perspective`), `src/cli.rs` (`render`), tests in `src/render.rs`. | The preview/final split is still simplified, but the core render outputs are working. | | Preview / final render workflow | VistaPro manuals describe rough preview rendering and full render output. | Implemented | `src/render.rs` (`render_top_down`, `render_perspective`), `src/cli.rs` (`render`), tests in `src/render.rs`. | The preview/final split is still simplified, but the core render outputs are working. |
| Render quality presets / smoothing / detail tradeoffs | VistaPro manuals describe quality menus and poly/detail tradeoffs. | Planned | No dedicated quality preset system in current code. | Add explicit quality presets or a render-quality profile object. | | Render quality presets / smoothing / detail tradeoffs | VistaPro manuals describe quality menus and poly/detail tradeoffs. | Implemented | `src/render.rs` (`RenderQualityPreset`, quality-aware top-down/perspective render helpers), `src/cli.rs` (`--quality`, output-path resolution), `src/app_state.rs`, `src/app.rs`, tests in `src/render.rs`, `src/app_state.rs`, and `src/cli.rs`. | The project-owned quality profile slice now toggles preview/balanced/final tradeoffs for both the CLI spike and the egui shell. |
| Scene file save/load (`.ovp.toml`) | Not a VistaPro legacy format; this is the clean-room OpenVistaPro scene format. | Implemented | `src/scene_file.rs`, `src/cli.rs` (`scene export`), tests in `src/scene_file.rs`. | No gap for the project-owned scene format slice. | | Scene file save/load (`.ovp.toml`) | Not a VistaPro legacy format; this is the clean-room OpenVistaPro scene format. | Implemented | `src/scene_file.rs`, `src/cli.rs` (`scene export`), tests in `src/scene_file.rs`. | No gap for the project-owned scene format slice. |
| Script language parser | MakePath guide and VistaPro manual describe scripts and “Run Script” workflows. | Implemented | `src/script.rs`, `src/script_exec.rs`, `src/cli.rs` (`script run`), `src/app_state.rs` script preview, `README.md` script section, tests in `src/script.rs` and `src/script_exec.rs`. | The project-owned grammar is now parsed and executed; any future work should focus on richer syntax or animation export, not basic parser support. | | Script language parser | MakePath guide and VistaPro manual describe scripts and “Run Script” workflows. | Implemented | `src/script.rs`, `src/script_exec.rs`, `src/cli.rs` (`script run`), `src/app_state.rs` script preview, `README.md` script section, tests in `src/script.rs` and `src/script_exec.rs`. | The project-owned grammar is now parsed and executed; any future work should focus on richer syntax or animation export, not basic parser support. |
| Script execution and animation frames | MakePath guide says scripts should render full animations and VistaPro can run scripts from the Script menu. | Implemented | `src/script_exec.rs` (`run_script` / `run_script_source`), `src/cli.rs` (`script run`), `src/app.rs` script controls, `src/app_state.rs`, tests in `src/script_exec.rs`. | The executor now runs preset/import/render slices; animation-frame sequencing is still a future extension. | | Script execution and animation frames | MakePath guide says scripts should render full animations and VistaPro can run scripts from the Script menu. | Implemented | `src/script_exec.rs` (`run_script` / `run_script_source`), `src/cli.rs` (`script run`), `src/app.rs` script controls, `src/app_state.rs`, tests in `src/script_exec.rs`. | The executor now runs preset/import/render slices; animation-frame sequencing is still a future extension. |
@@ -39,4 +39,4 @@ Notes:
## Current reconciliation summary ## Current reconciliation summary
OpenVistaPro already covers the core clean-room pipeline: terrain grids, open importers, scene state, preview/final rendering, project-owned scene files, script execution, MakePath-style path generation, editable color-map thresholds/bands, and scene-level vertical exaggeration. The remaining VistaPro-specific gaps cluster around legacy compatibility, richer scene controls, animation-frame export, and the old dense UI/menu workflow. OpenVistaPro already covers the core clean-room pipeline: terrain grids, open importers, scene state, preview/final rendering, quality-profile tradeoffs, project-owned scene files, script execution, MakePath-style path generation, editable color-map thresholds/bands, and scene-level vertical exaggeration. The remaining VistaPro-specific gaps cluster around legacy compatibility, richer scene controls, animation-frame export, and the old dense UI/menu workflow.
+1 -1
View File
@@ -9,7 +9,7 @@ This is a normalized modern shell map derived from the VistaPro manuals, screens
| Viewport / preview | Main render window, map preview, perspective view, preview/final render output | Center dock | Partial | `src/app.rs` renders the CPU preview into `CentralPanel`; perspective and top-down preview modes exist, but there is no GPU viewport or direct manipulation overlay yet. | | Viewport / preview | Main render window, map preview, perspective view, preview/final render output | Center dock | Partial | `src/app.rs` renders the CPU preview into `CentralPanel`; perspective and top-down preview modes exist, but there is no GPU viewport or direct manipulation overlay yet. |
| Terrain / import | Load Landscape, Import, terrain source selection, generated terrain presets | Left dock or collapsible section | Partial | The current shell exposes project-owned terrain presets (`Plane`, `RadialHill`) and a working heightmap import action; legacy format import UI is still absent. | | Terrain / import | Load Landscape, Import, terrain source selection, generated terrain presets | Left dock or collapsible section | Partial | The current shell exposes project-owned terrain presets (`Plane`, `RadialHill`) and a working heightmap import action; legacy format import UI is still absent. |
| Scene / camera | Camera and target gadgets, lens/range, bank/heading/pitch, water/tree/snow/haze controls | Left dock or inspector stack | Partial | Position/target, explicit heading/pitch/bank controls, lens/FOV/clip range controls, vertical exaggeration, color-map editing, and hydrology overlays now live in `src/app.rs` and `src/app_state.rs`; `src/scene.rs`, `src/render.rs`, and `src/script_exec.rs` carry the model/render semantics. The shell covers the main VistaPro scene controls, but its camera semantics are intentionally simplified and not yet tied to any map-click placement workflow. | | Scene / camera | Camera and target gadgets, lens/range, bank/heading/pitch, water/tree/snow/haze controls | Left dock or inspector stack | Partial | Position/target, explicit heading/pitch/bank controls, lens/FOV/clip range controls, vertical exaggeration, color-map editing, and hydrology overlays now live in `src/app.rs` and `src/app_state.rs`; `src/scene.rs`, `src/render.rs`, and `src/script_exec.rs` carry the model/render semantics. The shell covers the main VistaPro scene controls, but its camera semantics are intentionally simplified and not yet tied to any map-click placement workflow. |
| Render | Preview vs final render, quality/smoothing, detail tradeoffs | Left dock, toolbar, or render tab | Partial | Current code toggles top-down vs perspective render mode, but there is no dedicated quality profile or render preset UI. | | Render | Preview vs final render, quality/smoothing, detail tradeoffs | Left dock, toolbar, or render tab | Partial | Current code now exposes preview/balanced/final quality presets alongside top-down vs perspective render mode; the shell still lacks the full legacy menu chrome and fine-grained smoothing sliders. |
| Scripts / paths | Script menu, Run Script, MakePath path tools, animation-frame workflows | Right dock or modal workflow | Partial | Script parsing/execution and MakePath-style path generation now run end-to-end in the backend; the shell surfaces a script editor, Run Script, and Make Path controls, but animation-frame export and richer path editing are still future work. | | Scripts / paths | Script menu, Run Script, MakePath path tools, animation-frame workflows | Right dock or modal workflow | Partial | Script parsing/execution and MakePath-style path generation now run end-to-end in the backend; the shell surfaces a script editor, Run Script, and Make Path controls, but animation-frame export and richer path editing are still future work. |
| File / project actions | New/Open/Save landscape, scene load/save, export commands | Top bar / file menu | Partial | The shell now shows scene-file status and working New/Open/Save controls; legacy menu chrome and export dialogs are still missing. | | File / project actions | New/Open/Save landscape, scene load/save, export commands | Top bar / file menu | Partial | The shell now shows scene-file status and working New/Open/Save controls; legacy menu chrome and export dialogs are still missing. |
| Status / feedback | Coordinate readouts, render state, file path, progress, messages | Bottom status bar | Present | The shell now has a bottom status bar driven by `AppData::ui_snapshot()`. | | Status / feedback | Coordinate readouts, render state, file path, progress, messages | Bottom status bar | Present | The shell now has a bottom status bar driven by `AppData::ui_snapshot()`. |
+16
View File
@@ -508,6 +508,21 @@ mod tests {
assert_eq!(preview.height(), 64); assert_eq!(preview.height(), 64);
} }
#[test]
fn render_quality_changes_preview_output() {
let mut app = AppData::default();
app.apply(AppAction::SetPreviewSize {
width: 33,
height: 33,
});
let preview = app.render_preview().unwrap();
app.apply(AppAction::SetRenderQuality(RenderQuality::Final));
let final_img = app.render_preview().unwrap();
assert_ne!(preview.as_raw(), final_img.as_raw());
}
#[test] #[test]
fn ui_snapshot_exposes_existing_controls_and_new_entry_points() { fn ui_snapshot_exposes_existing_controls_and_new_entry_points() {
let app = AppData::default(); let app = AppData::default();
@@ -524,6 +539,7 @@ mod tests {
assert!(shell.import_path.is_some()); assert!(shell.import_path.is_some());
assert!(shell.path_target.is_none()); assert!(shell.path_target.is_none());
assert!(shell.status_line.contains("CPU preview")); assert!(shell.status_line.contains("CPU preview"));
assert!(shell.status_line.contains("Preview"));
} }
#[test] #[test]
+33 -104
View File
@@ -8,6 +8,8 @@ use crate::render::{
render_top_down_with_quality, render_top_down_with_quality,
}; };
use crate::scene::Scene;
use crate::scene_file::{self, SceneFileError}; use crate::scene_file::{self, SceneFileError};
use crate::script_exec::{self, ScriptError}; use crate::script_exec::{self, ScriptError};
use crate::terrain::{HeightGrid, TerrainError}; use crate::terrain::{HeightGrid, TerrainError};
@@ -169,6 +171,22 @@ pub fn supported_quality_presets() -> &'static [&'static str] {
&["preview", "balanced", "final"] &["preview", "balanced", "final"]
} }
pub fn resolve_render_output_path(
output: &std::path::Path,
preset: Preset,
quality: RenderQualityPreset,
) -> PathBuf {
if output.is_dir() {
output.join(format!(
"openvistapro-{}-{}.png",
preset.slug(),
quality.output_tag()
))
} else {
output.to_path_buf()
}
}
pub fn supported_importers() -> &'static [&'static str] { pub fn supported_importers() -> &'static [&'static str] {
#[cfg(all( #[cfg(all(
feature = "hgt", feature = "hgt",
@@ -242,24 +260,13 @@ pub fn info_text() -> String {
let mut text = String::new(); let mut text = String::new();
writeln!(&mut text, "openvistapro {}", env!("CARGO_PKG_VERSION")).unwrap(); writeln!(&mut text, "openvistapro {}", env!("CARGO_PKG_VERSION")).unwrap();
writeln!(&mut text, "presets: {}", supported_presets().join(", ")).unwrap(); writeln!(&mut text, "presets: {}", supported_presets().join(", ")).unwrap();
writeln!(
&mut text,
"quality presets: {}",
supported_quality_presets().join(", ")
)
.unwrap();
let importers = supported_importers(); let importers = supported_importers();
if importers.is_empty() { if importers.is_empty() {
writeln!(&mut text, "importers: (none)").unwrap(); writeln!(&mut text, "importers: (none)").unwrap();
} else { } else {
writeln!(&mut text, "importers: {}", importers.join(", ")).unwrap(); writeln!(&mut text, "importers: {}", importers.join(", ")).unwrap();
} }
writeln!( writeln!(&mut text, "scene files: .ovp.toml").unwrap();
&mut text,
"scene file: project-owned .ovp.toml (schema {})",
scene_file::SCENE_SCHEMA
)
.unwrap();
text text
} }
@@ -274,6 +281,7 @@ pub fn execute(cli: Cli) -> Result<(), CliError> {
Preset::Plane => HeightGrid::plane(args.width, args.height)?, Preset::Plane => HeightGrid::plane(args.width, args.height)?,
Preset::Hill => HeightGrid::radial_hill(args.width, args.height, HILL_PEAK_HEIGHT)?, Preset::Hill => HeightGrid::radial_hill(args.width, args.height, HILL_PEAK_HEIGHT)?,
}; };
let output = resolve_render_output_path(&args.output, args.preset, args.quality);
let mut scene = if let Some(path) = args.scene.as_deref() { let mut scene = if let Some(path) = args.scene.as_deref() {
scene_file::load_from_path(path)? scene_file::load_from_path(path)?
} else { } else {
@@ -281,24 +289,23 @@ pub fn execute(cli: Cli) -> Result<(), CliError> {
}; };
if args.camera_demo { if args.camera_demo {
scene.camera = demo_camera_for(&grid); scene.camera = demo_camera_for(&grid);
render_perspective_to_path(&grid, &scene, args.width, args.height, &args.output)?; let image = render_perspective_with_quality(
} else if args.quality == RenderQualityPreset::Preview {
render_top_down_to_path(&grid, &scene, &args.output)?;
} else {
let img = render_perspective_with_quality(
&grid, &grid,
&scene, &scene,
args.width, args.width,
args.height, args.height,
args.quality, args.quality,
); );
img.save(&args.output)?; image.save(&output)?;
} else {
let image = render_top_down_with_quality(&grid, &scene, args.quality);
image.save(&output)?;
} }
Ok(()) Ok(())
} }
Command::Scene(args) => match args.action { Command::Scene(args) => match args.action {
SceneAction::Export(export) => { SceneAction::Export(export) => {
scene_file::save_to_path(&Scene::default(), &export.output)?; scene_file::save_to_path(&crate::scene::Scene::default(), &export.output)?;
Ok(()) Ok(())
} }
}, },
@@ -317,6 +324,7 @@ pub fn execute(cli: Cli) -> Result<(), CliError> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::scene::Scene;
#[test] #[test]
fn parses_info_command() { fn parses_info_command() {
@@ -469,55 +477,6 @@ mod tests {
); );
} }
#[test]
fn info_text_lists_quality_presets() {
let text = info_text();
assert!(text.contains("preview"));
assert!(text.contains("balanced"));
assert!(text.contains("final"));
}
#[test]
fn supported_quality_presets_lists_preview_balanced_and_final() {
let qualities = supported_quality_presets();
assert!(qualities.contains(&"preview"));
assert!(qualities.contains(&"balanced"));
assert!(qualities.contains(&"final"));
}
#[test]
fn resolve_render_output_path_uses_quality_suffix_for_directories() {
let dir = temp_output_dir("quality-path");
let preview = resolve_render_output_path(&dir, Preset::Hill, RenderQualityPreset::Preview);
let final_path = resolve_render_output_path(&dir, Preset::Hill, RenderQualityPreset::Final);
assert_ne!(preview, final_path);
assert_eq!(preview.file_name().and_then(|s| s.to_str()), Some("openvistapro-hill-preview.png"));
assert_eq!(final_path.file_name().and_then(|s| s.to_str()), Some("openvistapro-hill-final.png"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn parses_render_with_quality_preset() {
let cli = Cli::try_parse_from([
"openvistapro",
"render",
"--preset",
"hill",
"--quality",
"final",
"--output",
"/tmp/out.png",
])
.unwrap();
match cli.command {
Command::Render(args) => {
assert_eq!(args.quality, RenderQualityPreset::Final);
}
_ => panic!("expected render"),
}
}
fn temp_output_path(tag: &str) -> PathBuf { fn temp_output_path(tag: &str) -> PathBuf {
let mut path = std::env::temp_dir(); let mut path = std::env::temp_dir();
path.push(format!( path.push(format!(
@@ -529,18 +488,6 @@ mod tests {
path path
} }
fn temp_output_dir(tag: &str) -> PathBuf {
let mut dir = std::env::temp_dir();
dir.push(format!(
"openvistapro-cli-{}-{}-dir",
tag,
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test] #[test]
fn execute_render_plane_writes_png_with_requested_dimensions() { fn execute_render_plane_writes_png_with_requested_dimensions() {
let path = temp_output_path("plane"); let path = temp_output_path("plane");
@@ -588,30 +535,6 @@ mod tests {
std::fs::remove_file(&path).ok(); std::fs::remove_file(&path).ok();
} }
#[test]
fn execute_render_uses_quality_specific_filename_for_output_directories() {
let dir = temp_output_dir("quality-exec");
let cli = Cli::try_parse_from([
"openvistapro",
"render",
"--preset",
"hill",
"--quality",
"final",
"--width",
"8",
"--height",
"8",
"--output",
dir.to_str().unwrap(),
])
.unwrap();
execute(cli).expect("execute should succeed");
let derived = dir.join("openvistapro-hill-final.png");
assert!(derived.exists(), "expected derived output path {derived:?}");
std::fs::remove_dir_all(&dir).ok();
}
#[test] #[test]
fn parses_render_with_camera_demo_flag() { fn parses_render_with_camera_demo_flag() {
let cli = Cli::try_parse_from([ let cli = Cli::try_parse_from([
@@ -831,6 +754,12 @@ mod tests {
water_level: 1000.0, water_level: 1000.0,
tree_line: 1001.0, tree_line: 1001.0,
snow_line: 1002.0, snow_line: 1002.0,
hydrology: crate::scene::Hydrology {
lake_radius: 0.0,
river_width: 0.0,
river_bend: 0.0,
..crate::scene::Hydrology::default()
},
..Scene::default() ..Scene::default()
}; };
crate::scene_file::save_to_path(&custom, &scene_path).expect("save scene"); crate::scene_file::save_to_path(&custom, &scene_path).expect("save scene");
+24
View File
@@ -526,6 +526,30 @@ mod tests {
assert_eq!(a.as_raw(), b.as_raw()); assert_eq!(a.as_raw(), b.as_raw());
} }
#[test]
fn render_quality_presets_change_sampling_and_blur() {
let preview = RenderQualityPreset::Preview.profile();
let balanced = RenderQualityPreset::Balanced.profile();
let final_profile = RenderQualityPreset::Final.profile();
assert_eq!(preview.label, "Preview");
assert_eq!(preview.top_down_blur_passes, 0);
assert!(preview.perspective_step > balanced.perspective_step);
assert!(balanced.perspective_step > final_profile.perspective_step);
assert!(balanced.top_down_blur_passes > preview.top_down_blur_passes);
assert!(final_profile.top_down_blur_passes > balanced.top_down_blur_passes);
}
#[test]
fn render_top_down_quality_changes_output_for_detail_presets() {
let grid = HeightGrid::radial_hill(33, 33, 10.0).unwrap();
let scene = fixture_scene();
let preview = render_top_down_with_quality(&grid, &scene, RenderQualityPreset::Preview);
let final_img = render_top_down_with_quality(&grid, &scene, RenderQualityPreset::Final);
assert_ne!(preview.as_raw(), final_img.as_raw());
}
#[test] #[test]
fn render_to_path_writes_png_file() { fn render_to_path_writes_png_file() {
let grid = HeightGrid::radial_hill(8, 8, 10.0).unwrap(); let grid = HeightGrid::radial_hill(8, 8, 10.0).unwrap();