feat: wire shell placeholders to backend actions #12
@@ -29,7 +29,7 @@ cargo run --features app --bin openvistapro_app
|
||||
```
|
||||
|
||||
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 and a CPU-rendered terrain preview.
|
||||
It opens an `eframe`/`egui` window titled `OpenVistaPro` with scene controls, file/status chrome, script/path placeholders, and a CPU-rendered terrain preview.
|
||||
|
||||
Importer status:
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ Notes:
|
||||
| Script language parser | MakePath guide and VistaPro manual describe scripts and “Run Script” workflows. | Partial | `src/script.rs` parser, tests in `src/script.rs`, `README.md` script section. | Parser exists, but script execution is intentionally deferred. |
|
||||
| Script execution and animation frames | MakePath guide says scripts should render full animations and VistaPro can run scripts from the Script menu. | Planned | No script runner or frame-sequencing engine exists yet. | Add execution semantics once the command model is stable. |
|
||||
| MakePath-style path generation and motion models | MakePath guide describes spline nodes, previewing a path, and vehicle models (jet, glider, dune buggy, motorcycle, helicopter, cruise missile, custom). | Planned | No path generator or motion-model layer exists yet. | This is a separate planner/animation feature, not just a script parser. |
|
||||
| UI shell, menus, dialogs, and numeric gadgets | VistaPro screenshots/manuals show dense menus, dialogs, map tools, and numeric gadgets. | Partial | `src/app.rs`, `src/app_state.rs`, `src/bin/openvistapro_app.rs`. | Current UI is an egui CPU-preview shell with a small control set, not the legacy menu hierarchy. |
|
||||
| Modern UI shell, menus, dialogs, and numeric gadgets | VistaPro screenshots/manuals show dense menus, dialogs, map tools, and numeric gadgets. | Partial | `src/app.rs`, `src/app_state.rs`, `src/bin/openvistapro_app.rs`, `docs/knowledgebase/ui-panel-map.md`. | Current UI is a dockable egui CPU-preview shell with terrain, scene/camera, render, viewport, file/status, and scripts/paths surfaces; import/path execution and legacy dialogs remain disabled or planned. |
|
||||
| Legacy image / landscape export formats | VistaPro manuals mention saving rendered images and landscapes in formats like IFF/IFF24/RGB and DEM/binary landscape files. | Planned | Current output is PNG plus project-owned `.ovp.toml` scenes. | Add separate compatibility/export work only after the clean internal pipeline is stable. |
|
||||
|
||||
## Current reconciliation summary
|
||||
|
||||
OpenVistaPro already covers the core clean-room pipeline: terrain grids, open importers, scene state, preview/final rendering, project-owned scene files, and a small script parser. The remaining VistaPro-specific gaps cluster around legacy compatibility, richer scene controls, script execution, MakePath-style animation tooling, and the old dense UI/menu workflow.
|
||||
OpenVistaPro already covers the core clean-room pipeline: terrain grids, open importers, scene state, preview/final rendering, project-owned scene files, and a small script parser. The remaining VistaPro-specific gaps cluster around legacy compatibility, richer scene controls, script execution, MakePath-style animation tooling, and the modernized docked shell work needed to replace the old dense UI/menu workflow.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# OpenVistaPro UI Panel Map
|
||||
|
||||
This is a normalized modern shell map derived from the VistaPro manuals, screenshots, and the current OpenVistaPro codebase. It is intentionally not a 1:1 recreation of the legacy menu hierarchy; instead, it groups the old surfaces into a docked shell that can grow with the product.
|
||||
|
||||
## Proposed panel layout
|
||||
|
||||
| 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` already 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 placeholder import entry point; 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 and scene-band sliders exist. Lens/range, orientation axes, and richer VistaPro camera semantics are still missing. |
|
||||
| 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. |
|
||||
| Scripts / paths | Script menu, Run Script, MakePath path tools, animation-frame workflows | Right dock or modal workflow | Partial | Script parsing exists in the codebase, the shell now surfaces a script editor and placeholder path controls, but execution and path generation are still deferred. |
|
||||
| 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 disabled file actions; load/save execution is still wired only in the backend modules. |
|
||||
| Status / feedback | Coordinate readouts, render state, file path, progress, messages | Bottom status bar | Partial | The shell now has a bottom status bar driven by `AppData::ui_snapshot()`. |
|
||||
| Deferred features / legacy compatibility | Dense menu hierarchy, advanced lighting, hydrology, vertical exaggeration, palette editing, legacy image/landscape exports | Right dock, tool drawer, or dialogs | Planned | These are best surfaced as future tabs or dialogs rather than cluttering the initial shell. |
|
||||
|
||||
## Recommended shell structure
|
||||
|
||||
A practical first-pass shell is:
|
||||
|
||||
1. Top bar for file/project actions and render mode shortcuts.
|
||||
2. Left dock for terrain/import and scene/camera controls.
|
||||
3. Center viewport for preview output.
|
||||
4. Right dock for scripts/paths and deferred advanced features.
|
||||
5. Bottom status bar for current scene, source, and render feedback.
|
||||
|
||||
That layout preserves the VistaPro workflow while making room for modern discoverability and incremental feature growth.
|
||||
|
||||
## Panel-by-panel implementation summary
|
||||
|
||||
| Panel | Code support today | Implementation evidence | Priority |
|
||||
|---|---|---|---|
|
||||
| Viewport / preview | Present | `src/app.rs` renders the preview into `CentralPanel`; `src/app_state.rs` builds the preview image. | High |
|
||||
| Terrain / import | Partial | `TerrainPreset` and `AppAction::SetTerrainPreset` in `src/app_state.rs`; terrain radio buttons in `src/app.rs`. | High |
|
||||
| Scene / camera | Partial | `Scene`, camera position/target controls, and scene-band sliders in `src/app.rs` and `src/app_state.rs`. | High |
|
||||
| Render | Partial | `RendererMode` plus preview switching in `src/app_state.rs` and `src/app.rs`. | High |
|
||||
| Scripts / paths | Not yet | `docs/knowledgebase/feature-inventory.md` marks scripts and path generation as planned. | Medium |
|
||||
| File / project actions | Not yet | `loaded_scene_path` exists in `AppData`, but there is no visible file/project UI yet. | Medium |
|
||||
| Status / feedback | Not yet | No dedicated status widget or state binding is present. | Medium |
|
||||
| Deferred features / legacy compatibility | Not yet | These remain future work in the feature inventory. | Low |
|
||||
|
||||
## Remaining gaps
|
||||
|
||||
- No legacy-style menu/dialog layer for file, export, or script workflows.
|
||||
- No docked status bar or live feedback line.
|
||||
- No UI for orientation, lens/range, or vertical exaggeration.
|
||||
- No dedicated scripts/paths editor surface.
|
||||
- No palette editor, hydrology controls, or legacy export panels.
|
||||
+154
-61
@@ -1,7 +1,7 @@
|
||||
use eframe::egui;
|
||||
use image::RgbImage;
|
||||
|
||||
use crate::app_state::{AppAction, AppData, RendererMode, TerrainPreset};
|
||||
use crate::app_state::{AppAction, AppData, RendererMode, TerrainPreset, UiShellSnapshot};
|
||||
use crate::scene::Vec3;
|
||||
|
||||
pub const WINDOW_TITLE: &str = "OpenVistaPro";
|
||||
@@ -39,76 +39,169 @@ impl eframe::App for OpenVistaProApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
let mut changed = false;
|
||||
|
||||
egui::SidePanel::left("scene_controls").show(ctx, |ui| {
|
||||
ui.heading("OpenVistaPro");
|
||||
ui.label("CPU preview shell");
|
||||
egui::SidePanel::left("scene_controls")
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
ui.heading("OpenVistaPro");
|
||||
ui.label("CPU preview shell");
|
||||
|
||||
ui.separator();
|
||||
ui.label("Terrain");
|
||||
let mut preset = self.data.terrain_preset;
|
||||
changed |= ui
|
||||
.radio_value(&mut preset, TerrainPreset::RadialHill, "Radial hill")
|
||||
.changed();
|
||||
changed |= ui
|
||||
.radio_value(&mut preset, TerrainPreset::Plane, "Plane")
|
||||
.changed();
|
||||
if preset != self.data.terrain_preset {
|
||||
self.data.apply(AppAction::SetTerrainPreset(preset));
|
||||
}
|
||||
ui.separator();
|
||||
ui.label("Terrain");
|
||||
let mut preset = self.data.terrain_preset;
|
||||
changed |= ui
|
||||
.radio_value(&mut preset, TerrainPreset::RadialHill, "Radial hill")
|
||||
.changed();
|
||||
changed |= ui
|
||||
.radio_value(&mut preset, TerrainPreset::Plane, "Plane")
|
||||
.changed();
|
||||
if preset != self.data.terrain_preset {
|
||||
self.data.apply(AppAction::SetTerrainPreset(preset));
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.label("Renderer");
|
||||
let mut renderer_mode = self.data.renderer_mode;
|
||||
changed |= ui
|
||||
.radio_value(&mut renderer_mode, RendererMode::TopDown, "Top-down")
|
||||
.changed();
|
||||
changed |= ui
|
||||
.radio_value(&mut renderer_mode, RendererMode::Perspective, "Perspective")
|
||||
.changed();
|
||||
if renderer_mode != self.data.renderer_mode {
|
||||
self.data.apply(AppAction::SetRendererMode(renderer_mode));
|
||||
}
|
||||
ui.separator();
|
||||
ui.label("Renderer");
|
||||
let mut renderer_mode = self.data.renderer_mode;
|
||||
changed |= ui
|
||||
.radio_value(&mut renderer_mode, RendererMode::TopDown, "Top-down")
|
||||
.changed();
|
||||
changed |= ui
|
||||
.radio_value(&mut renderer_mode, RendererMode::Perspective, "Perspective")
|
||||
.changed();
|
||||
if renderer_mode != self.data.renderer_mode {
|
||||
self.data.apply(AppAction::SetRendererMode(renderer_mode));
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.label("Scene bands");
|
||||
let mut water = self.data.scene.water_level;
|
||||
let mut trees = self.data.scene.tree_line;
|
||||
let mut snow = self.data.scene.snow_line;
|
||||
let mut haze = self.data.scene.haze;
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut water, -5.0..=10.0).text("Water"))
|
||||
.changed();
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut trees, -5.0..=12.0).text("Trees"))
|
||||
.changed();
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut snow, -5.0..=15.0).text("Snow"))
|
||||
.changed();
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut haze, 0.0..=1.0).text("Haze"))
|
||||
.changed();
|
||||
self.data.apply(AppAction::SetWaterLevel(water));
|
||||
self.data.apply(AppAction::SetTreeLine(trees));
|
||||
self.data.apply(AppAction::SetSnowLine(snow));
|
||||
self.data.apply(AppAction::SetHaze(haze));
|
||||
ui.separator();
|
||||
ui.label("Scene bands");
|
||||
let mut water = self.data.scene.water_level;
|
||||
let mut trees = self.data.scene.tree_line;
|
||||
let mut snow = self.data.scene.snow_line;
|
||||
let mut haze = self.data.scene.haze;
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut water, -5.0..=10.0).text("Water"))
|
||||
.changed();
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut trees, -5.0..=12.0).text("Trees"))
|
||||
.changed();
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut snow, -5.0..=15.0).text("Snow"))
|
||||
.changed();
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut haze, 0.0..=1.0).text("Haze"))
|
||||
.changed();
|
||||
self.data.apply(AppAction::SetWaterLevel(water));
|
||||
self.data.apply(AppAction::SetTreeLine(trees));
|
||||
self.data.apply(AppAction::SetSnowLine(snow));
|
||||
self.data.apply(AppAction::SetHaze(haze));
|
||||
|
||||
ui.separator();
|
||||
ui.label("Camera");
|
||||
let mut camera_position = self.data.scene.camera.position;
|
||||
let mut camera_target = self.data.scene.camera.target;
|
||||
changed |= vec3_controls(ui, "Position", &mut camera_position);
|
||||
changed |= vec3_controls(ui, "Target", &mut camera_target);
|
||||
self.data
|
||||
.apply(AppAction::SetCameraPosition(camera_position));
|
||||
self.data.apply(AppAction::SetCameraTarget(camera_target));
|
||||
});
|
||||
ui.separator();
|
||||
ui.label("Camera");
|
||||
let mut camera_position = self.data.scene.camera.position;
|
||||
let mut camera_target = self.data.scene.camera.target;
|
||||
changed |= vec3_controls(ui, "Position", &mut camera_position);
|
||||
changed |= vec3_controls(ui, "Target", &mut camera_target);
|
||||
self.data
|
||||
.apply(AppAction::SetCameraPosition(camera_position));
|
||||
self.data.apply(AppAction::SetCameraTarget(camera_target));
|
||||
});
|
||||
|
||||
egui::SidePanel::right("entry_points")
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
ui.heading("Scripts / paths");
|
||||
|
||||
ui.separator();
|
||||
ui.label("Import terrain");
|
||||
ui.horizontal(|ui| {
|
||||
let import_path = self
|
||||
.data
|
||||
.import_path
|
||||
.as_deref()
|
||||
.unwrap_or("No import path selected");
|
||||
ui.monospace(import_path);
|
||||
ui.add_enabled(false, egui::Button::new("Import heightmap…"));
|
||||
});
|
||||
ui.label(
|
||||
"Legacy import surfaces remain planned; the shell only shows the entry point.",
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
ui.label("Script source");
|
||||
changed |= ui
|
||||
.add(
|
||||
egui::TextEdit::multiline(&mut self.data.script_source)
|
||||
.desired_rows(8)
|
||||
.lock_focus(true)
|
||||
.desired_width(f32::INFINITY),
|
||||
)
|
||||
.changed();
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled(false, egui::Button::new("Run script"));
|
||||
ui.label("Parser-only MVP; execution stays disabled.");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.label("Path tools");
|
||||
ui.horizontal(|ui| {
|
||||
let path_target = self
|
||||
.data
|
||||
.path_target
|
||||
.as_deref()
|
||||
.unwrap_or("No path target selected");
|
||||
ui.monospace(path_target);
|
||||
ui.add_enabled(false, egui::Button::new("Make path"));
|
||||
});
|
||||
ui.label(
|
||||
"Path generation is still a planned feature; only the entry point is visible.",
|
||||
);
|
||||
});
|
||||
|
||||
if changed || self.texture.is_none() {
|
||||
self.rebuild_texture(ctx);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
let snapshot = self.data.ui_snapshot();
|
||||
|
||||
egui::TopBottomPanel::top("project_bar").show(ctx, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(snapshot.scene_file_label.as_str());
|
||||
match snapshot.scene_file_path.as_deref() {
|
||||
Some(path) => ui.monospace(path),
|
||||
None => ui.weak("No scene file loaded"),
|
||||
};
|
||||
ui.separator();
|
||||
ui.add_enabled(false, egui::Button::new("New"));
|
||||
ui.add_enabled(false, egui::Button::new("Open…"));
|
||||
ui.add_enabled(false, egui::Button::new("Save"));
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(snapshot.status_line.as_str());
|
||||
ui.separator();
|
||||
ui.monospace(format!(
|
||||
"scripts: {} cmd / {} render / {} import",
|
||||
snapshot.script_preview.command_count,
|
||||
snapshot.script_preview.render_commands,
|
||||
snapshot.script_preview.import_commands,
|
||||
));
|
||||
if let Some(error) = snapshot.script_preview.error.as_deref() {
|
||||
ui.colored_label(ui.visuals().error_fg_color, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("Preview");
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Preview");
|
||||
ui.label(format!(
|
||||
"{} · {}",
|
||||
snapshot.terrain_preset_label, snapshot.renderer_mode_label
|
||||
));
|
||||
});
|
||||
ui.separator();
|
||||
if let Some(texture) = &self.texture {
|
||||
ui.image((texture.id(), texture.size_vec2()));
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ use image::RgbImage;
|
||||
|
||||
use crate::render::{render_perspective, render_top_down};
|
||||
use crate::scene::{Scene, Vec3};
|
||||
use crate::script::{Command, parse_script};
|
||||
use crate::terrain::{HeightGrid, TerrainError};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -17,6 +18,14 @@ impl TerrainPreset {
|
||||
TerrainPreset::RadialHill => HeightGrid::radial_hill(width, height, 10.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable label used by the UI shell.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
TerrainPreset::Plane => "Plane",
|
||||
TerrainPreset::RadialHill => "Radial hill",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -25,6 +34,82 @@ pub enum RendererMode {
|
||||
Perspective,
|
||||
}
|
||||
|
||||
impl RendererMode {
|
||||
/// Human-readable label used by the UI shell.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
RendererMode::TopDown => "Top-down",
|
||||
RendererMode::Perspective => "Perspective",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pure, derived summary of a parsed script, suitable for display in the UI.
|
||||
///
|
||||
/// All counts are zero and `error` carries the parser message when the script
|
||||
/// fails to parse, so the shell can render a single consistent preview shape.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct ScriptPreview {
|
||||
/// Total number of parsed commands.
|
||||
pub command_count: usize,
|
||||
/// Number of `render output` commands.
|
||||
pub render_commands: usize,
|
||||
/// Number of `import heightmap` commands.
|
||||
pub import_commands: usize,
|
||||
/// Parser error message, if the script did not parse.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl ScriptPreview {
|
||||
/// Build a preview by running the backend script parser over `source`.
|
||||
fn from_source(source: &str) -> Self {
|
||||
match parse_script(source) {
|
||||
Ok(script) => {
|
||||
let render_commands = script
|
||||
.commands
|
||||
.iter()
|
||||
.filter(|command| matches!(command, Command::RenderOutput { .. }))
|
||||
.count();
|
||||
let import_commands = script
|
||||
.commands
|
||||
.iter()
|
||||
.filter(|command| matches!(command, Command::ImportHeightmap { .. }))
|
||||
.count();
|
||||
Self {
|
||||
command_count: script.commands.len(),
|
||||
render_commands,
|
||||
import_commands,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
Err(error) => Self {
|
||||
error: Some(error.to_string()),
|
||||
..Self::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pure, immutable snapshot of the UI shell state.
|
||||
///
|
||||
/// The egui app reads this each frame instead of reaching into [`AppData`]
|
||||
/// directly. Labels are stable strings; optional values are `None` when the
|
||||
/// corresponding entry point has not been wired to a real workflow yet.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UiShellSnapshot {
|
||||
pub terrain_preset_label: String,
|
||||
pub renderer_mode_label: String,
|
||||
pub scene_file_label: String,
|
||||
pub import_label: String,
|
||||
pub script_label: String,
|
||||
pub path_label: String,
|
||||
pub scene_file_path: Option<String>,
|
||||
pub import_path: Option<String>,
|
||||
pub path_target: Option<String>,
|
||||
pub status_line: String,
|
||||
pub script_preview: ScriptPreview,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AppData {
|
||||
pub scene: Scene,
|
||||
@@ -32,6 +117,12 @@ pub struct AppData {
|
||||
pub renderer_mode: RendererMode,
|
||||
pub preview_size: (u32, u32),
|
||||
pub loaded_scene_path: Option<String>,
|
||||
/// Heightmap path selected for import; `None` until import is wired up.
|
||||
pub import_path: Option<String>,
|
||||
/// Path-tool target; `None` until path generation is wired up.
|
||||
pub path_target: Option<String>,
|
||||
/// Current script source text edited in the Scripts / paths panel.
|
||||
pub script_source: String,
|
||||
}
|
||||
|
||||
impl Default for AppData {
|
||||
@@ -42,6 +133,9 @@ impl Default for AppData {
|
||||
renderer_mode: RendererMode::TopDown,
|
||||
preview_size: (256, 256),
|
||||
loaded_scene_path: None,
|
||||
import_path: None,
|
||||
path_target: None,
|
||||
script_source: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +155,29 @@ impl AppData {
|
||||
self.preview_size = (width.max(1), height.max(1));
|
||||
}
|
||||
AppAction::SetLoadedScenePath(path) => self.loaded_scene_path = path,
|
||||
AppAction::SetScriptSource(source) => self.script_source = source,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a pure snapshot of the UI shell state for the egui app to render.
|
||||
pub fn ui_snapshot(&self) -> UiShellSnapshot {
|
||||
let (width, height) = self.preview_size;
|
||||
UiShellSnapshot {
|
||||
terrain_preset_label: self.terrain_preset.label().to_string(),
|
||||
renderer_mode_label: self.renderer_mode.label().to_string(),
|
||||
scene_file_label: "Scene file".to_string(),
|
||||
import_label: "Import terrain".to_string(),
|
||||
script_label: "Scripts / paths".to_string(),
|
||||
path_label: "Path tools".to_string(),
|
||||
scene_file_path: self.loaded_scene_path.clone(),
|
||||
import_path: self.import_path.clone(),
|
||||
path_target: self.path_target.clone(),
|
||||
status_line: format!(
|
||||
"CPU preview · {} · {} · {width}×{height}",
|
||||
self.terrain_preset.label(),
|
||||
self.renderer_mode.label(),
|
||||
),
|
||||
script_preview: ScriptPreview::from_source(&self.script_source),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +209,7 @@ pub enum AppAction {
|
||||
SetCameraTarget(Vec3),
|
||||
SetPreviewSize { width: u32, height: u32 },
|
||||
SetLoadedScenePath(Option<String>),
|
||||
SetScriptSource(String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -165,6 +283,38 @@ mod tests {
|
||||
assert_eq!(preview.height(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_snapshot_exposes_existing_controls_and_new_entry_points() {
|
||||
let app = AppData::default();
|
||||
let shell = app.ui_snapshot();
|
||||
|
||||
assert_eq!(shell.terrain_preset_label, "Radial hill");
|
||||
assert_eq!(shell.renderer_mode_label, "Top-down");
|
||||
assert_eq!(shell.scene_file_label, "Scene file");
|
||||
assert_eq!(shell.import_label, "Import terrain");
|
||||
assert_eq!(shell.script_label, "Scripts / paths");
|
||||
assert_eq!(shell.path_label, "Path tools");
|
||||
assert!(shell.scene_file_path.is_none());
|
||||
assert!(shell.import_path.is_none());
|
||||
assert!(shell.path_target.is_none());
|
||||
assert!(shell.status_line.contains("CPU preview"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_snapshot_uses_backend_script_parser_for_command_counts() {
|
||||
let mut app = AppData::default();
|
||||
app.apply(AppAction::SetScriptSource(
|
||||
"use preset plane\nrender output \"out.png\"\n".into(),
|
||||
));
|
||||
|
||||
let shell = app.ui_snapshot();
|
||||
|
||||
assert_eq!(shell.script_preview.command_count, 2);
|
||||
assert_eq!(shell.script_preview.render_commands, 1);
|
||||
assert_eq!(shell.script_preview.import_commands, 0);
|
||||
assert!(shell.script_preview.error.is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "app"))]
|
||||
#[test]
|
||||
fn app_feature_is_declared_but_not_enabled_by_default() {
|
||||
|
||||
Reference in New Issue
Block a user