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