feat: add GUI terrain generation controls #19
@@ -33,9 +33,18 @@ cargo run --features app --bin openvistapro_app
|
|||||||
The crate’s default `cargo run` target is the CLI binary (`openvistapro`), so the commands above work without an extra `--bin` flag. Use `--features app --bin openvistapro_app` for the optional GUI shell.
|
The crate’s default `cargo run` target is the CLI binary (`openvistapro`), so the commands above work without an extra `--bin` flag. Use `--features app --bin openvistapro_app` for the optional GUI shell.
|
||||||
|
|
||||||
The optional app shell is gated behind the `app` feature so default CLI builds stay GPU-free.
|
The optional app shell is gated behind the `app` feature so default CLI builds stay GPU-free.
|
||||||
It opens an `eframe`/`egui` window titled `OpenVistaPro` with scene controls, top-level menus/dialogs, and a CPU-rendered terrain preview.
|
It opens an `eframe`/`egui` window titled `OpenVistaPro` with scene controls, top-level menus/dialogs, a Terrain section, and a CPU-rendered terrain preview.
|
||||||
The native shell uses eframe's Glow renderer under X11/Xvfb, which keeps the app smoke launch independent of WGPU backend feature selection.
|
The native shell uses eframe's Glow renderer under X11/Xvfb, which keeps the app smoke launch independent of WGPU backend feature selection.
|
||||||
|
|
||||||
|
Terrain workflow in the GUI:
|
||||||
|
|
||||||
|
- Open the Terrain section in the left dock, or use Terrain → Generate current preset from the top menu.
|
||||||
|
- Choose Plane, Radial hill, or Fractal noise; when Fractal noise is selected, edit the Fractal seed field.
|
||||||
|
- Click Generate current preset to build the active preset into the live preview.
|
||||||
|
- Generated terrain updates the preview directly; if generation fails, the previous imported terrain stays in place.
|
||||||
|
|
||||||
|
This GUI path is interactive preview-first. The CLI `cargo run -- render --preset fractal --seed 1337 ...` path writes an output image, and script workflows use `use preset fractal` plus `render output ...` to drive the same generation boundary non-interactively.
|
||||||
|
|
||||||
Importer status:
|
Importer status:
|
||||||
|
|
||||||
- `heightmap`: project-owned script input that imports grayscale PNG heightmaps with `import heightmap "path.png"` and maps brightness to elevation. This is a terrain-ingest convenience path, not a legacy VistaPro compatibility claim.
|
- `heightmap`: project-owned script input that imports grayscale PNG heightmaps with `import heightmap "path.png"` and maps brightness to elevation. This is a terrain-ingest convenience path, not a legacy VistaPro compatibility claim.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This is a normalized modern shell map derived from the VistaPro manuals, screens
|
|||||||
| Modern panel | VistaPro surfaces it absorbs | Suggested placement | Current code support | Notes / gaps |
|
| Modern panel | VistaPro surfaces it absorbs | Suggested placement | Current code support | Notes / gaps |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 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`, `Radial hill`, `Fractal noise`) plus a `Fractal seed` field and `Generate current preset` action, alongside a working heightmap import action; the GUI builds terrain into the live preview, while CLI/script workflows drive the same generator non-interactively. 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, lighting direction/intensity gadgets, 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, lighting direction/intensity gadgets, 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 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. |
|
| 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. |
|
||||||
|
|||||||
+51
-2
@@ -138,6 +138,13 @@ impl OpenVistaProApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.menu_button("Terrain", |ui| {
|
||||||
|
if ui.button("Generate current preset").clicked() {
|
||||||
|
changed |= self.generate_current_terrain(action_note);
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ui.menu_button("Tools", |ui| {
|
ui.menu_button("Tools", |ui| {
|
||||||
if ui.button("Run script").clicked() {
|
if ui.button("Run script").clicked() {
|
||||||
let base_dir = std::path::Path::new(Self::default_script_base_dir());
|
let base_dir = std::path::Path::new(Self::default_script_base_dir());
|
||||||
@@ -200,7 +207,7 @@ impl OpenVistaProApp {
|
|||||||
ui.label(self.shell.section_summary());
|
ui.label(self.shell.section_summary());
|
||||||
ui.separator();
|
ui.separator();
|
||||||
match self.shell.active_section {
|
match self.shell.active_section {
|
||||||
ShellSection::Terrain => changed |= self.terrain_controls(ui),
|
ShellSection::Terrain => changed |= self.terrain_controls(ui, action_note),
|
||||||
ShellSection::Scene => changed |= self.scene_controls(ui),
|
ShellSection::Scene => changed |= self.scene_controls(ui),
|
||||||
ShellSection::Render => changed |= self.render_controls(ui),
|
ShellSection::Render => changed |= self.render_controls(ui),
|
||||||
ShellSection::Import => changed |= self.import_controls(ui, action_note),
|
ShellSection::Import => changed |= self.import_controls(ui, action_note),
|
||||||
@@ -212,7 +219,7 @@ impl OpenVistaProApp {
|
|||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn terrain_controls(&mut self, ui: &mut egui::Ui) -> bool {
|
fn terrain_controls(&mut self, ui: &mut egui::Ui, action_note: &mut Option<String>) -> bool {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
let mut preset = self.data.terrain_preset;
|
let mut preset = self.data.terrain_preset;
|
||||||
changed |= ui
|
changed |= ui
|
||||||
@@ -234,6 +241,29 @@ impl OpenVistaProApp {
|
|||||||
self.data.apply(AppAction::SetTerrainPreset(preset));
|
self.data.apply(AppAction::SetTerrainPreset(preset));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Generate current preset").clicked() {
|
||||||
|
changed |= self.generate_current_terrain(action_note);
|
||||||
|
}
|
||||||
|
ui.label("Build the active terrain preset into the preview.");
|
||||||
|
});
|
||||||
|
if let Some(grid) = self.data.generated_grid.as_ref() {
|
||||||
|
ui.label(format!(
|
||||||
|
"Generated terrain: {}×{}",
|
||||||
|
grid.width(),
|
||||||
|
grid.height()
|
||||||
|
));
|
||||||
|
} else if let Some(grid) = self.data.imported_grid.as_ref() {
|
||||||
|
ui.label(format!(
|
||||||
|
"Imported terrain: {}×{}",
|
||||||
|
grid.width(),
|
||||||
|
grid.height()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ui.label("The preview is still showing the current preset directly.");
|
||||||
|
}
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label("Fractal seed");
|
ui.label("Fractal seed");
|
||||||
let mut seed = self.data.terrain_seed;
|
let mut seed = self.data.terrain_seed;
|
||||||
@@ -248,6 +278,25 @@ impl OpenVistaProApp {
|
|||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_current_terrain(&mut self, action_note: &mut Option<String>) -> bool {
|
||||||
|
let terrain_label = self.data.terrain_preset.label();
|
||||||
|
match self.data.generate_terrain() {
|
||||||
|
Ok(grid) => {
|
||||||
|
*action_note = Some(format!(
|
||||||
|
"generated {} terrain into {}×{} preview",
|
||||||
|
terrain_label,
|
||||||
|
grid.width(),
|
||||||
|
grid.height()
|
||||||
|
));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
*action_note = Some(format!("terrain generation failed: {error}"));
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_controls(&mut self, ui: &mut egui::Ui) -> bool {
|
fn render_controls(&mut self, ui: &mut egui::Ui) -> bool {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
let mut renderer_mode = self.data.renderer_mode;
|
let mut renderer_mode = self.data.renderer_mode;
|
||||||
|
|||||||
+137
-3
@@ -168,6 +168,9 @@ pub struct AppData {
|
|||||||
pub import_path: Option<String>,
|
pub import_path: Option<String>,
|
||||||
/// Path-tool target or summary.
|
/// Path-tool target or summary.
|
||||||
pub path_target: Option<String>,
|
pub path_target: Option<String>,
|
||||||
|
/// Generated terrain currently active in the preview, if the shell has
|
||||||
|
/// explicitly generated the current preset.
|
||||||
|
pub generated_grid: Option<HeightGrid>,
|
||||||
/// Current script source text edited in the Scripts / paths panel.
|
/// Current script source text edited in the Scripts / paths panel.
|
||||||
pub script_source: String,
|
pub script_source: String,
|
||||||
/// Height grid currently imported through the shell.
|
/// Height grid currently imported through the shell.
|
||||||
@@ -194,6 +197,7 @@ impl Default for AppData {
|
|||||||
loaded_scene_path: Some(scene_path),
|
loaded_scene_path: Some(scene_path),
|
||||||
import_path: Some(import_path.clone()),
|
import_path: Some(import_path.clone()),
|
||||||
path_target: None,
|
path_target: None,
|
||||||
|
generated_grid: None,
|
||||||
script_source: format!(
|
script_source: format!(
|
||||||
"use preset hill\nimport heightmap \"{import_path}\"\nset thresholds water=1.0 tree=4.0 snow=7.0\nrender output \"{script_output}\"\n"
|
"use preset hill\nimport heightmap \"{import_path}\"\nset thresholds water=1.0 tree=4.0 snow=7.0\nrender output \"{script_output}\"\n"
|
||||||
),
|
),
|
||||||
@@ -207,8 +211,18 @@ impl Default for AppData {
|
|||||||
impl AppData {
|
impl AppData {
|
||||||
pub fn apply(&mut self, action: AppAction) {
|
pub fn apply(&mut self, action: AppAction) {
|
||||||
match action {
|
match action {
|
||||||
AppAction::SetTerrainPreset(preset) => self.terrain_preset = preset,
|
AppAction::SetTerrainPreset(preset) => {
|
||||||
AppAction::SetTerrainSeed(seed) => self.terrain_seed = seed,
|
self.terrain_preset = preset;
|
||||||
|
if self.generated_grid.is_some() {
|
||||||
|
let _ = self.refresh_generated_grid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppAction::SetTerrainSeed(seed) => {
|
||||||
|
self.terrain_seed = seed;
|
||||||
|
if self.generated_grid.is_some() {
|
||||||
|
let _ = self.refresh_generated_grid();
|
||||||
|
}
|
||||||
|
}
|
||||||
AppAction::SetRendererMode(mode) => self.renderer_mode = mode,
|
AppAction::SetRendererMode(mode) => self.renderer_mode = mode,
|
||||||
AppAction::SetRenderQuality(quality) => self.render_quality = quality,
|
AppAction::SetRenderQuality(quality) => self.render_quality = quality,
|
||||||
AppAction::SetWaterLevel(value) => self.scene.water_level = value,
|
AppAction::SetWaterLevel(value) => self.scene.water_level = value,
|
||||||
@@ -275,6 +289,9 @@ impl AppData {
|
|||||||
AppAction::SetPalette(palette) => self.scene.palette = palette,
|
AppAction::SetPalette(palette) => self.scene.palette = palette,
|
||||||
AppAction::SetPreviewSize { width, height } => {
|
AppAction::SetPreviewSize { width, height } => {
|
||||||
self.preview_size = (width.max(1), height.max(1));
|
self.preview_size = (width.max(1), height.max(1));
|
||||||
|
if self.generated_grid.is_some() {
|
||||||
|
let _ = self.refresh_generated_grid();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppAction::SetLoadedScenePath(path) => self.loaded_scene_path = path,
|
AppAction::SetLoadedScenePath(path) => self.loaded_scene_path = path,
|
||||||
AppAction::SetScriptSource(source) => self.script_source = source,
|
AppAction::SetScriptSource(source) => self.script_source = source,
|
||||||
@@ -283,6 +300,7 @@ impl AppData {
|
|||||||
|
|
||||||
pub fn reset_scene(&mut self) {
|
pub fn reset_scene(&mut self) {
|
||||||
self.scene = Scene::default();
|
self.scene = Scene::default();
|
||||||
|
self.generated_grid = None;
|
||||||
self.imported_grid = None;
|
self.imported_grid = None;
|
||||||
self.generated_path = None;
|
self.generated_path = None;
|
||||||
self.last_script_run = None;
|
self.last_script_run = None;
|
||||||
@@ -295,6 +313,7 @@ impl AppData {
|
|||||||
pub fn open_scene(&mut self, path: &Path) -> Result<(), SceneFileError> {
|
pub fn open_scene(&mut self, path: &Path) -> Result<(), SceneFileError> {
|
||||||
let scene = scene_file::load_from_path(path)?;
|
let scene = scene_file::load_from_path(path)?;
|
||||||
self.scene = scene;
|
self.scene = scene;
|
||||||
|
self.generated_grid = None;
|
||||||
self.imported_grid = None;
|
self.imported_grid = None;
|
||||||
self.generated_path = None;
|
self.generated_path = None;
|
||||||
self.last_script_run = None;
|
self.last_script_run = None;
|
||||||
@@ -314,6 +333,7 @@ impl AppData {
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let source = std::fs::read_to_string(path)?;
|
let source = std::fs::read_to_string(path)?;
|
||||||
let imported = crate::import::import_ovp_text(&source)?;
|
let imported = crate::import::import_ovp_text(&source)?;
|
||||||
|
self.generated_grid = None;
|
||||||
self.imported_grid = Some(imported.into_grid());
|
self.imported_grid = Some(imported.into_grid());
|
||||||
self.import_path = Some(path.display().to_string());
|
self.import_path = Some(path.display().to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -325,6 +345,15 @@ impl AppData {
|
|||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates the current terrain preset and makes it the active preview source.
|
||||||
|
pub fn generate_terrain(&mut self) -> Result<&HeightGrid, TerrainError> {
|
||||||
|
self.refresh_generated_grid()?;
|
||||||
|
Ok(self
|
||||||
|
.generated_grid
|
||||||
|
.as_ref()
|
||||||
|
.expect("generated grid just refreshed"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn make_path(&mut self) -> CameraPath {
|
pub fn make_path(&mut self) -> CameraPath {
|
||||||
let path = build_demo_path(&self.scene);
|
let path = build_demo_path(&self.scene);
|
||||||
self.path_target = Some(format!(
|
self.path_target = Some(format!(
|
||||||
@@ -338,6 +367,9 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn active_height_grid(&self) -> Result<HeightGrid, TerrainError> {
|
fn active_height_grid(&self) -> Result<HeightGrid, TerrainError> {
|
||||||
|
if let Some(grid) = &self.generated_grid {
|
||||||
|
return Ok(grid.clone());
|
||||||
|
}
|
||||||
if let Some(grid) = &self.imported_grid {
|
if let Some(grid) = &self.imported_grid {
|
||||||
return Ok(grid.clone());
|
return Ok(grid.clone());
|
||||||
}
|
}
|
||||||
@@ -370,7 +402,8 @@ impl AppData {
|
|||||||
import_path: self.import_path.clone(),
|
import_path: self.import_path.clone(),
|
||||||
path_target: self.path_target.clone(),
|
path_target: self.path_target.clone(),
|
||||||
status_line: format!(
|
status_line: format!(
|
||||||
"CPU preview · {} · {} · {} · exag {:.2} · {width}×{height}",
|
"CPU preview · {} · {} · {} · {} · exag {:.2} · {width}×{height}",
|
||||||
|
self.terrain_source_label(),
|
||||||
terrain_status,
|
terrain_status,
|
||||||
self.renderer_mode.label(),
|
self.renderer_mode.label(),
|
||||||
self.render_quality.label(),
|
self.render_quality.label(),
|
||||||
@@ -384,6 +417,24 @@ impl AppData {
|
|||||||
self.active_height_grid()
|
self.active_height_grid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refresh_generated_grid(&mut self) -> Result<(), TerrainError> {
|
||||||
|
let (width, height) = self.preview_size;
|
||||||
|
let grid = self.terrain_preset.build_grid(width, height)?;
|
||||||
|
self.imported_grid = None;
|
||||||
|
self.generated_grid = Some(grid);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terrain_source_label(&self) -> &'static str {
|
||||||
|
if self.generated_grid.is_some() {
|
||||||
|
"generated terrain"
|
||||||
|
} else if self.imported_grid.is_some() {
|
||||||
|
"imported terrain"
|
||||||
|
} else {
|
||||||
|
"preset terrain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_preview(&self) -> Result<RgbImage, TerrainError> {
|
pub fn render_preview(&self) -> Result<RgbImage, TerrainError> {
|
||||||
let grid = self.build_preview_grid()?;
|
let grid = self.build_preview_grid()?;
|
||||||
let (width, height) = self.preview_size;
|
let (width, height) = self.preview_size;
|
||||||
@@ -461,6 +512,7 @@ mod tests {
|
|||||||
assert_eq!(app.preview_size, (256, 256));
|
assert_eq!(app.preview_size, (256, 256));
|
||||||
assert!(app.loaded_scene_path.is_some());
|
assert!(app.loaded_scene_path.is_some());
|
||||||
assert!(app.script_source.contains("import heightmap"));
|
assert!(app.script_source.contains("import heightmap"));
|
||||||
|
assert!(app.generated_grid.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -584,6 +636,49 @@ mod tests {
|
|||||||
assert_ne!(preview.as_raw(), final_img.as_raw());
|
assert_ne!(preview.as_raw(), final_img.as_raw());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perspective_renderer_changes_the_gui_preview_path() {
|
||||||
|
let mut app = AppData::default();
|
||||||
|
app.apply(AppAction::SetPreviewSize {
|
||||||
|
width: 48,
|
||||||
|
height: 32,
|
||||||
|
});
|
||||||
|
|
||||||
|
let top_down = app.render_preview().unwrap();
|
||||||
|
app.apply(AppAction::SetRendererMode(RendererMode::Perspective));
|
||||||
|
let perspective = app.render_preview().unwrap();
|
||||||
|
let snapshot = app.ui_snapshot();
|
||||||
|
|
||||||
|
assert_eq!(perspective.width(), 48);
|
||||||
|
assert_eq!(perspective.height(), 32);
|
||||||
|
assert_ne!(top_down.as_raw(), perspective.as_raw());
|
||||||
|
assert!(snapshot.status_line.contains("Perspective"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fractal_seed_changes_the_gui_preview_terrain() {
|
||||||
|
let mut app = AppData::default();
|
||||||
|
app.apply(AppAction::SetPreviewSize {
|
||||||
|
width: 48,
|
||||||
|
height: 32,
|
||||||
|
});
|
||||||
|
app.apply(AppAction::SetRendererMode(RendererMode::Perspective));
|
||||||
|
app.apply(AppAction::SetTerrainPreset(TerrainPreset::Fractal {
|
||||||
|
seed: 2,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let seed_two = app.render_preview().unwrap();
|
||||||
|
app.apply(AppAction::SetTerrainSeed(9001));
|
||||||
|
app.apply(AppAction::SetTerrainPreset(TerrainPreset::Fractal {
|
||||||
|
seed: 9001,
|
||||||
|
}));
|
||||||
|
let seed_nine_thousand_one = app.render_preview().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(seed_two.width(), 48);
|
||||||
|
assert_eq!(seed_two.height(), 32);
|
||||||
|
assert_ne!(seed_two.as_raw(), seed_nine_thousand_one.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();
|
||||||
@@ -601,6 +696,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("preset terrain"));
|
||||||
assert!(shell.status_line.contains("Preview"));
|
assert!(shell.status_line.contains("Preview"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,6 +730,44 @@ mod tests {
|
|||||||
assert!(app.imported_grid.is_some());
|
assert!(app.imported_grid.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_terrain_replaces_imported_preview_source() {
|
||||||
|
let mut app = AppData::default();
|
||||||
|
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests/fixtures/open/tiny-heightfield.ovptext");
|
||||||
|
|
||||||
|
app.import_heightmap_from_path(&fixture)
|
||||||
|
.expect("import fixture");
|
||||||
|
let preview_size = app.preview_size;
|
||||||
|
let generated_dims = {
|
||||||
|
let generated = app.generate_terrain().expect("generate terrain");
|
||||||
|
(generated.width(), generated.height())
|
||||||
|
};
|
||||||
|
let grid = app.build_preview_grid().expect("preview grid");
|
||||||
|
|
||||||
|
assert_eq!(generated_dims.0, preview_size.0);
|
||||||
|
assert_eq!(generated_dims.1, preview_size.1);
|
||||||
|
assert_eq!(grid.width(), preview_size.0);
|
||||||
|
assert!(app.imported_grid.is_none());
|
||||||
|
assert!(app.generated_grid.is_some());
|
||||||
|
assert!(app.ui_snapshot().status_line.contains("generated terrain"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_terrain_tracks_preset_changes() {
|
||||||
|
let mut app = AppData::default();
|
||||||
|
app.generate_terrain().expect("generate terrain");
|
||||||
|
let before = app
|
||||||
|
.build_preview_grid()
|
||||||
|
.expect("preview grid before change");
|
||||||
|
|
||||||
|
app.apply(AppAction::SetTerrainPreset(TerrainPreset::Plane));
|
||||||
|
let after = app.build_preview_grid().expect("preview grid after change");
|
||||||
|
|
||||||
|
assert!(before.min_max().expect("generated grid has min/max").1 > 0.0);
|
||||||
|
assert_eq!(after.min_max(), Some((0.0, 0.0)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn make_path_produces_a_three_keyframe_summary() {
|
fn make_path_produces_a_three_keyframe_summary() {
|
||||||
let mut app = AppData::default();
|
let mut app = AppData::default();
|
||||||
|
|||||||
Reference in New Issue
Block a user