facett-demo/src/bin/osm_fly_demo.rs
//! **facett — OSM-Fly demo** (`osm_fly_demo`): the ONE connected flow.
//!
//! Pick a region (Sweden) → **clip** the cached planet OSM to the Sweden polygon
//! (with an animated progress bar sweeping the WHOLE time — clip AND load AND
//! scene-build, one continuous story) → **fly** the clipped result in 3D with the
//! 6DOF flight camera. The planet source + the clip are **cached on a large disk**
//! (`$FACETT_OSM_CACHE` or `/mnt/work4t/facett-osm-cache`, NEVER T9), keyed by region
//! + source-hash so a second run skips the clip and loads straight to flight.
//!
//! The heavy wiring lives in [`facett_demo::osm_fly`]; this bin is the eframe shell:
//! a progress screen while the background pipeline clips/loads, then the flight view.
//!
//! ## Honesty
//! A real planet `.pbf` is usually absent on a dev box. When none is found the flow
//! does NOT fake it — it logs the fallback notice and flies the embedded Sigtuna clip
//! (a real slice of Sweden) over the procedural DEM. Drop `planet.osm.pbf` under the
//! cache root (or set `$FACETT_PLANET`) for the full planet→Sweden clip run.
//!
//! Run:
//! ```bash
//! cargo run -p facett-demo --bin osm_fly_demo # fallback (no planet)
//! FACETT_PLANET=/data/planet.osm.pbf cargo run -p facett-demo --bin osm_fly_demo --release
//! ```
//!
//! Flight controls (the crate's `ControlFrame::from_egui` mapping): **W/A/S/D** move,
//! **R/F** climb/dive, **drag** to look, **Shift** boost, **Q/E** roll, **1/2/3** mode.
// Native-only (osm-katana clip + threads + fs + flight paint). Empty `main` on wasm.
#[cfg(target_arch = "wasm32")]
fn main() {}
#[cfg(not(target_arch = "wasm32"))]
use app::App;
#[cfg(not(target_arch = "wasm32"))]
mod app {
use eframe::egui;
use facett::pop::{ProgressItem, ProgressView};
use facett_demo::osm_fly::{
self, FlyConfig, FlyMsg, FlyPhase, FlyReport, FlyWorld,
};
use facett_map3d_flight::ControlFrame;
use std::sync::mpsc::Receiver;
/// The app is in one of two states: the progress screen while the background
/// pipeline clips+loads, then the live flight over the clipped region.
pub struct App {
/// The clip→load pipeline channel (drained each frame). `None` after we've
/// built the world (pipeline done).
rx: Option<Receiver<FlyMsg>>,
/// The current phase of the one continuous progress story.
phase: FlyPhase,
/// The animated progress bar (a `facett-pop` ProgressView), spanning ALL phases.
progress: ProgressView,
/// Recent log lines (cache hit/miss, fallback notice, summary).
log: Vec<String>,
/// The flyable world, once the pipeline is ready.
world: Option<FlyWorld>,
/// The report (region / cache-hit / real-clip), once ready.
report: Option<FlyReport>,
/// HUD toggle.
show_hud: bool,
}
impl App {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// Kick the flow off immediately on a background thread — Sweden preset.
let rx = osm_fly::spawn_flow(FlyConfig::sweden());
Self {
rx: Some(rx),
phase: FlyPhase::Idle,
progress: ProgressView::new("osm-fly-progress"),
log: Vec::new(),
world: None,
report: None,
show_hud: true,
}
}
/// Drain the pipeline channel: advance the phase, collect logs, and (on Ready)
/// build the flyable world.
fn poll_pipeline(&mut self) {
let Some(rx) = self.rx.as_ref() else { return };
let mut done = false;
loop {
match rx.try_recv() {
Ok(FlyMsg::Phase(p)) => self.phase = p,
Ok(FlyMsg::Log(s)) => {
self.log.push(s);
if self.log.len() > 8 {
let drop = self.log.len() - 8;
self.log.drain(0..drop);
}
}
Ok(FlyMsg::Ready(geo, report)) => {
self.world = Some(FlyWorld::from_geometry(&geo));
self.report = Some(*report);
}
Ok(FlyMsg::Failed(e)) => {
self.log.push(format!("FAILED: {e}"));
self.phase = FlyPhase::Error;
done = true;
}
Err(std::sync::mpsc::TryRecvError::Empty) => break,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
done = true;
break;
}
}
}
// Only leave the progress screen once BOTH the world is built and the
// pipeline signalled Flying (so the bar completes its story first).
if done || (self.world.is_some() && self.phase == FlyPhase::Flying) {
self.rx = None;
}
}
/// Refresh the single continuous progress bar from the current phase + a
/// time-driven sweep (so it visibly MOVES even though the clip exposes no
/// fine-grained progress). One bar, one story: clip → load → build → fly.
fn update_progress(&mut self, ctx: &egui::Context) {
let t = ctx.input(|i| i.time);
let sweep = ((t / 1.5).rem_euclid(1.0)) as f32;
let frac = self.phase.fraction(sweep);
let step = match self.phase {
FlyPhase::Idle => "starting",
FlyPhase::Clipping => "clipping planet → Sweden",
FlyPhase::Loading => "loading clipped GeoParquet",
FlyPhase::Building => "building flight scene",
FlyPhase::Flying => "ready",
FlyPhase::Error => "error",
};
self.progress.phase = format!("{step} ({:.0}%)", frac * 100.0);
self.progress.items =
vec![ProgressItem { label: "sweden".to_string(), done: (frac * 1000.0) as u64, total: 1000 }];
}
/// The progress screen (phase 1).
fn ui_loading(&mut self, ui: &mut egui::Ui) {
ui.ctx().request_repaint(); // keep the bar animating
self.update_progress(ui.ctx());
egui::CentralPanel::default().show(ui, |ui| {
ui.vertical_centered(|ui| {
ui.add_space(ui.available_height() * 0.30);
ui.heading("facett · OSM-Fly");
ui.label("Sweden — clip the planet, then fly it.");
ui.add_space(12.0);
ui.set_max_width(520.0);
let _ = self.progress.view(ui);
ui.add_space(10.0);
for line in &self.log {
ui.small(line);
}
});
});
}
/// The flight view (phase 2).
fn ui_flight(&mut self, ui: &mut egui::Ui) {
ui.ctx().request_repaint();
let dt = (ui.input(|i| i.stable_dt) as f64).clamp(1.0e-4, 0.1);
egui::Panel::top("bar").show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
ui.heading("facett · OSM-Fly");
ui.separator();
if let Some(w) = &self.world {
ui.label(&w.label);
}
ui.separator();
if let Some(r) = &self.report {
let tag = if r.real_clip {
if r.cache_hit { "clip: cache HIT" } else { "clip: fresh" }
} else {
"clip: fallback (no planet)"
};
ui.label(tag);
}
ui.separator();
ui.checkbox(&mut self.show_hud, "HUD");
});
});
egui::CentralPanel::default().show(ui, |ui| {
let size = ui.available_size();
// The input is sampled off a zero-height sense strip rather than a painter
// covering the pane: the pane itself is now drawn by `Map3D`, which wants a
// `Ui` and does its own allocation. Reading the drag from a rect the renderer
// also owns would fight it for the same pointer.
let resp = ui.interact(
egui::Rect::from_min_size(ui.next_widget_position(), size),
ui.id().with("fly_input"),
egui::Sense::click_and_drag(),
);
let rect = resp.rect;
let captured = resp.dragged();
let frame = ui.ctx().input(|i| ControlFrame::from_egui(i, captured));
if let Some(w) = self.world.as_mut() {
w.step(&frame, dt);
}
if let Some(w) = self.world.as_mut() {
osm_fly::paint_world(w, ui);
let painter = ui.painter_at(rect);
if self.show_hud {
let (lon, lat, alt) = w.cam_geo();
let fps = ui.ctx().input(|i| 1.0 / i.stable_dt.max(1e-4));
let hud = format!(
"mode: {}\nalt: {:.0} m lon/lat: {:.3}, {:.3}\nheading: {:.0}° pitch: {:.0}°\nFPS ≈ {:.0}\n\nWASD move · R/F climb/dive · drag to look\nShift boost · Q/E roll · 1/2/3 mode",
w.cam.mode.name(),
alt,
lon,
lat,
w.cam.yaw.to_degrees().rem_euclid(360.0),
w.cam.pitch.to_degrees(),
fps,
);
painter.text(
rect.left_top() + egui::vec2(10.0, 8.0),
egui::Align2::LEFT_TOP,
hud,
egui::FontId::monospace(13.0),
egui::Color32::from_rgb(20, 28, 36),
);
}
}
});
}
}
impl eframe::App for App {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.poll_pipeline();
if self.rx.is_some() || self.world.is_none() {
self.ui_loading(ui);
} else {
self.ui_flight(ui);
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
let native_options = eframe::NativeOptions {
viewport: eframe::egui::ViewportBuilder::default()
.with_inner_size([1100.0, 760.0])
.with_title("facett — OSM-Fly (Sweden)"),
renderer: eframe::Renderer::Wgpu,
// Force the wgpu backend onto the **discrete** NVIDIA GPU, REUSING the exact
// same tested policy the main demo binary installs (`gpu_select`): Vulkan-only
// backends, `HighPerformance`, and an adapter selector that rejects the
// llvmpipe/lavapipe `Cpu` software rasteriser and prefers the DiscreteGpu.
// Without this the flight demo took eframe's DEFAULT adapter pick, which on
// Loki (three Vulkan ICDs: llvmpipe + AMD iGPU + NVIDIA RTX) repeatedly landed
// on software/iGPU (~0.1 fps) — the flight's egui meshes were being
// software-rasterised. Now the SAME meshes go to the RTX 5080, so the flight
// is smooth. The chosen adapter is logged at startup (verifiable vs nvidia-smi).
wgpu_options: facett_demo::gpu_select::discrete_gpu_wgpu_options(),
..Default::default()
};
eframe::run_native(
"facett-osm-fly",
native_options,
Box::new(|cc| Ok(Box::new(App::new(cc)))),
)
}