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/osm3d_demo.rs

//! **facett — standalone 3D OSM viewer demo** (`osm3d_demo`).
//!
//! A self-contained eframe/wgpu app that opens the **state-of-the-art 3D map
//! viewer** ([`facett_map3d::Map3D`]): OSM ways extruded into a lit, perspective
//! 3D city you can orbit, dolly (zoom-to-cursor), pan, and snap to canonical
//! view-angle presets. This is the "test application" entry point — it does NOT
//! touch the shared `facett-demo` binary.
//!
//! ## Data
//! - If **`$FACETT_OSM_PARQUET`** points at an `osm2geoparquet` `ways.parquet`,
//!   the viewer extrudes the real ways (buildings → prisms, roads → ribbons).
//! - Otherwise it builds a **synthetic procedural city** (a grid of building
//!   footprints + a road network) so the demo runs **fixture-free**. Set
//!   `FACETT_OSM_RABBIT=1` to use the znippy-rabbit synthetic skyline instead.
//!
//! ## Navigation
//! - **Left-drag** orbit · **middle/Shift-drag** pan · **wheel / pinch** dolly
//!   (toward the cursor) · **WASD / arrows** orbit · **+ / -** zoom · **R** reset
//! - **1** top-down · **2** isometric · **3** oblique · **4** front
//!
//! Run:
//! ```bash
//! cargo run -p facett-demo --bin osm3d_demo                       # synthetic city
//! FACETT_OSM_PARQUET=/tmp/li_geo/ways.parquet \
//!     cargo run -p facett-demo --bin osm3d_demo --release         # real OSM
//! ```

// Native-only standalone 3D viewer (drives `eframe::run_native`). Excluded from
// the wasm web demo; compiles to an empty `main` on wasm so the package builds for
// `wasm32-unknown-unknown`.
#[cfg(target_arch = "wasm32")]
fn main() {}

#[cfg(not(target_arch = "wasm32"))]
use eframe::egui;
#[cfg(not(target_arch = "wasm32"))]
use facett::map::Way;
#[cfg(not(target_arch = "wasm32"))]
use facett::Facet;
#[cfg(not(target_arch = "wasm32"))]
use facett::look::Theme as LookTheme;
#[cfg(not(target_arch = "wasm32"))]
use facett_map3d::{Map3D, TerrainField, ViewPreset};

/// Build the ways the viewer extrudes: real OSM if `$FACETT_OSM_PARQUET` is set,
/// the znippy rabbit if `FACETT_OSM_RABBIT=1`, else a synthetic procedural city.
#[cfg(not(target_arch = "wasm32"))]
fn load_ways() -> (String, Vec<Way>) {
    if let Ok(path) = std::env::var("FACETT_OSM_PARQUET") {
        match facett::osm::read_ways(&path, 20_000, &facett::Theme::default()) {
            Ok(ways) if !ways.is_empty() => {
                return (format!("OSM · {} ways · {path}", ways.len()), ways);
            }
            Ok(_) => eprintln!("[osm3d] {path}: no ways decoded, falling back to synthetic"),
            Err(e) => eprintln!("[osm3d] {path}: {e}, falling back to synthetic"),
        }
    }
    if std::env::var("FACETT_OSM_RABBIT").is_ok() {
        return ("synthetic · znippy rabbit skyline".into(), Vec::new()); // signalled below
    }
    let ways = synthetic_city();
    (format!("synthetic city · {} footprints", ways.len()), ways)
}

/// A deterministic procedural city: a grid of building footprints (closed,
/// filled rings → prisms) interleaved with a road network (open lines →
/// ribbons). All in a small lon/lat patch so it projects through the same
/// Mercator path real OSM data uses. No RNG — reproducible.
#[cfg(not(target_arch = "wasm32"))]
fn synthetic_city() -> Vec<Way> {
    let lon0 = 9.50;
    let lat0 = 47.10;
    let cell = 0.0016; // degrees per block
    let n = 8; // n×n blocks
    let mut ways = Vec::new();
    let mut id = 1i64;

    // A simple deterministic hash → varied building heights / footprints.
    let hash = |a: i64, b: i64| -> u32 {
        let mut h = (a.wrapping_mul(73856093) ^ b.wrapping_mul(19349663)) as u32;
        h ^= h >> 13;
        h = h.wrapping_mul(0x9E3779B1);
        h ^ (h >> 16)
    };

    for gy in 0..n {
        for gx in 0..n {
            let h = hash(gx, gy);
            // Leave some plots empty (parks/squares) for variety.
            if h % 7 == 0 {
                continue;
            }
            let cx = lon0 + (gx as f64 + 0.5) * cell;
            let cy = lat0 + (gy as f64 + 0.5) * cell;
            // Footprint size: 35-48% of the cell.
            let half = cell * (0.18 + (h % 13) as f64 * 0.012);
            let ring = vec![
                (cx - half, cy - half),
                (cx + half, cy - half),
                (cx + half, cy + half),
                (cx - half, cy + half),
                (cx - half, cy - half), // closed
            ];
            // Cool-blue glass towers, tinted by the hash so the skyline reads.
            let r = 90 + (h % 60) as u8;
            let g = 150 + (h % 70) as u8;
            let b = 210 + (h % 45) as u8;
            let fill = egui::Color32::from_rgb(r, g, b);
            let outline = egui::Color32::from_rgb(20, 40, 60);
            ways.push(Way::filled(id, "building", ring, fill, outline, 1.0));
            id += 1;
        }
    }

    // Road grid: horizontal + vertical avenues between the blocks.
    let road = egui::Color32::from_rgb(70, 78, 90);
    for k in 0..=n {
        let lat = lat0 + k as f64 * cell;
        ways.push(Way::line(
            id,
            "highway",
            vec![(lon0 - cell * 0.2, lat), (lon0 + (n as f64 + 0.2) * cell, lat)],
            road,
            2.0,
        ));
        id += 1;
        let lon = lon0 + k as f64 * cell;
        ways.push(Way::line(
            id,
            "highway",
            vec![(lon, lat0 - cell * 0.2), (lon, lat0 + (n as f64 + 0.2) * cell)],
            road,
            2.0,
        ));
        id += 1;
    }
    ways
}

#[cfg(not(target_arch = "wasm32"))]
struct App {
    map: Map3D,
    label: String,
    show_state: bool,
}

#[cfg(not(target_arch = "wasm32"))]
impl App {
    fn new(cc: &eframe::CreationContext<'_>) -> Self {
        // Honor the OS look: applies palette + the OS-native camera feel.
        let os = cc.egui_ctx.os();
        LookTheme::from_os(os).apply(&cc.egui_ctx);

        let (label, ways) = load_ways();
        let map = if ways.is_empty() {
            // FACETT_OSM_RABBIT path: the znippy-rabbit synthetic skyline.
            Map3D::synthetic("3D OSM viewer").with_os_feel(os)
        } else {
            // Real DEM/relief terrain under the city (#24a): break the flat plane. A
            // real SRTM `.hgt` via `$FACETT_HGT`, else the procedural fallback.
            let (elev, real) = TerrainField::from_env();
            eprintln!("[osm3d] terrain: {}", if real { "real DEM (.hgt)" } else { "procedural fallback (set $FACETT_HGT for a real tile)" });
            Map3D::from_geometry_relief("3D OSM viewer", &ways, &elev, 64, 3.5).with_os_feel(os)
        };
        Self { map, label, show_state: true }
    }
}

#[cfg(not(target_arch = "wasm32"))]
impl eframe::App for App {
    // This facett-demo eframe build hands the app a single full-window `Ui`
    // (the same `App::ui` signature `main.rs` uses), so we lay panels out with
    // the `Ui`-taking `show` (egui 0.36 renamed `show_inside` to it; the old
    // `ctx`-based `show` is gone).
    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        egui::Panel::top("bar").show(ui, |ui| {
            ui.horizontal_wrapped(|ui| {
                ui.heading("facett · 3D OSM viewer");
                ui.separator();
                ui.label(&self.label);
                ui.separator();
                if ui.button("⬇ Top (1)").clicked() {
                    self.map.view_preset(ViewPreset::Top);
                }
                if ui.button("⬔ Iso (2)").clicked() {
                    self.map.view_preset(ViewPreset::Isometric);
                }
                if ui.button("◇ Oblique (3)").clicked() {
                    self.map.view_preset(ViewPreset::Oblique);
                }
                if ui.button("□ Front (4)").clicked() {
                    self.map.view_preset(ViewPreset::Front);
                }
                if ui.button("⟲ Reset (R)").clicked() {
                    self.map.reset_view();
                }
                ui.checkbox(&mut self.map.spin, "spin");
                ui.checkbox(&mut self.show_state, "state");
            });
        });

        if self.show_state {
            egui::Panel::right("state").min_size(260.0).show(ui, |ui| {
                ui.heading("Camera / introspection");
                ui.label(format!("FPS ≈ {:.0}", self.map.fps()));
                ui.label(format!(
                    "visible triangles: {} / {}",
                    self.map.visible_triangles(),
                    self.map.triangle_count()
                ));
                ui.separator();
                ui.label("state_json (LAW: components expose what they render):");
                egui::ScrollArea::vertical().show(ui, |ui| {
                    let pretty = serde_json::to_string_pretty(&self.map.state_json())
                        .unwrap_or_default();
                    ui.monospace(pretty);
                });
            });
        }

        egui::CentralPanel::default().show(ui, |ui| {
            Facet::ui(&mut self.map, ui);
        });
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
    let native_options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([1100.0, 760.0])
            .with_title("facett — 3D OSM viewer"),
        // The 3D map uses egui's painter (CPU perspective path), but we run the
        // wgpu backend so the host matches the rest of facett-demo.
        renderer: eframe::Renderer::Wgpu,
        ..Default::default()
    };
    eframe::run_native(
        "facett-osm3d",
        native_options,
        Box::new(|cc| Ok(Box::new(App::new(cc)))),
    )
}