Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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)))),
    )
}