facett_core/engine/source.rs
1//! **The three traits** — the ONLY things that differ between a map view and a
2//! graph view.
3//!
4//! Everything else a viewer does (cull, tessellate, place labels, composite,
5//! pick) is identical whether the thing on screen is a road or a call edge. This
6//! module names the difference and nothing else:
7//!
8//! | trait | question it answers | map answer | graph answer |
9//! |---|---|---|---|
10//! | [`PositionSource`] | *where is feature `id`?* | Mercator(lat, lon) | the layout's node position |
11//! | [`ElevationSource`] | *how high is it?* | terrain heightfield | `0.0`, or a metric (centrality / risk) |
12//! | [`Hierarchy`] | *what does "one step deeper" mean?* | the next zoom band | expand this cluster |
13//!
14//! ## Why `f64` world space
15//! Web-Mercator at zoom 22 and a graph drilled six levels deep have the *same*
16//! bug: `f32` runs out of mantissa and geometry jitters. The engine therefore
17//! keeps world space in `f64` and only becomes `f32` at the **relative-to-eye**
18//! split ([`super::rte`]) — one fix, both problems.
19//!
20//! ## Why object-safe
21//! Use case 7 — *a graph drawn on geography* — needs **two different position
22//! sources live in the same frame**. That is only expressible if the engine can
23//! hold `&[&dyn PositionSource]`, so every method here is object-safe: no
24//! generic methods, no `Self: Sized`, no associated types on the hot path.
25
26use std::fmt::Debug;
27
28// ─────────────────────────────────────────────────────────────────────────────
29// World space
30// ─────────────────────────────────────────────────────────────────────────────
31
32/// A position in **engine world space**, always `f64`.
33///
34/// What one unit *means* is the [`PositionSource`]'s business: Mercator unit
35/// square for a map, layout units for a graph, logical pixels for a flat
36/// overlay. The engine only ever subtracts, compares and bounds them.
37#[derive(Clone, Copy, Debug, Default, PartialEq)]
38pub struct WorldPos {
39 pub x: f64,
40 pub y: f64,
41 /// Filled by the [`ElevationSource`]; a [`PositionSource`] should leave it `0.0`.
42 pub z: f64,
43}
44
45impl WorldPos {
46 #[must_use]
47 pub const fn new(x: f64, y: f64, z: f64) -> Self {
48 Self { x, y, z }
49 }
50 #[must_use]
51 pub const fn flat(x: f64, y: f64) -> Self {
52 Self { x, y, z: 0.0 }
53 }
54 #[must_use]
55 pub fn is_finite(&self) -> bool {
56 self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
57 }
58}
59
60/// An axis-aligned world-space box — the culler's unit of work, and the only
61/// thing the [`Hierarchy`] levels are really made of (a coarse level is a set of
62/// bounding volumes that stand in for the fine level beneath it).
63#[derive(Clone, Copy, Debug, PartialEq)]
64pub struct WorldAabb {
65 pub min: WorldPos,
66 pub max: WorldPos,
67}
68
69impl WorldAabb {
70 /// The degenerate box at a point — what a POI, a pin or a graph node has.
71 #[must_use]
72 pub fn point(p: WorldPos) -> Self {
73 Self { min: p, max: p }
74 }
75
76 /// An empty box that absorbs correctly under [`union`](Self::union).
77 #[must_use]
78 pub fn empty() -> Self {
79 Self {
80 min: WorldPos::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
81 max: WorldPos::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
82 }
83 }
84
85 #[must_use]
86 pub fn is_empty(&self) -> bool {
87 self.min.x > self.max.x || self.min.y > self.max.y
88 }
89
90 #[must_use]
91 pub fn union(self, o: Self) -> Self {
92 Self {
93 min: WorldPos::new(self.min.x.min(o.min.x), self.min.y.min(o.min.y), self.min.z.min(o.min.z)),
94 max: WorldPos::new(self.max.x.max(o.max.x), self.max.y.max(o.max.y), self.max.z.max(o.max.z)),
95 }
96 }
97
98 /// 2D overlap test — the culler's inner predicate. Z is deliberately ignored:
99 /// a top-down or tilted view culls in the ground plane and lets the depth
100 /// buffer sort height out.
101 #[must_use]
102 pub fn overlaps_2d(&self, o: &Self) -> bool {
103 self.min.x <= o.max.x && self.max.x >= o.min.x && self.min.y <= o.max.y && self.max.y >= o.min.y
104 }
105
106 #[must_use]
107 pub fn centre(&self) -> WorldPos {
108 WorldPos::new(
109 (self.min.x + self.max.x) * 0.5,
110 (self.min.y + self.max.y) * 0.5,
111 (self.min.z + self.max.z) * 0.5,
112 )
113 }
114}
115
116// ─────────────────────────────────────────────────────────────────────────────
117// 1. PositionSource — Mercator | Layout | Identity
118// ─────────────────────────────────────────────────────────────────────────────
119
120/// **Where is feature `id`?**
121///
122/// The single seam between "this is a map" and "this is a graph". A road's
123/// position comes from Mercator; a node's comes from a force layout; an overlay
124/// glyph's is already in logical pixels. Downstream — culling, tessellation,
125/// label collision, picking — cannot tell which it got.
126pub trait PositionSource: Debug {
127 /// How many features this source addresses. Ids are `0..feature_count()`.
128 fn feature_count(&self) -> usize;
129
130 /// The anchor of feature `id` in world space, `z` left at `0.0`.
131 ///
132 /// Out-of-range ids must return a **non-finite** position rather than
133 /// panicking — a stale id from a previous frame is a normal event and the
134 /// engine filters non-finite positions out before any work.
135 fn position(&self, id: u32) -> WorldPos;
136
137 /// The world-space extent of feature `id`. Point features keep the default.
138 /// A road, a building footprint or a cluster hull overrides it — that box is
139 /// what the culler and the [`Hierarchy`] index.
140 fn bounds(&self, id: u32) -> WorldAabb {
141 WorldAabb::point(self.position(id))
142 }
143
144 /// Batch projection — **the hot path**. Overriding it is how a columnar
145 /// source (Arrow, a GPU-resident buffer) avoids a virtual call per feature.
146 fn positions(&self, ids: &[u32], out: &mut Vec<WorldPos>) {
147 out.clear();
148 out.reserve(ids.len());
149 out.extend(ids.iter().map(|&i| self.position(i)));
150 }
151
152 /// A **generation counter**. Bump it whenever positions move, so caches
153 /// (cull index, pick index, screen grids) know to rebuild. A static source
154 /// returns a constant; a live force layout returns its iteration number.
155 ///
156 /// This is the whole reason [`Layout`] is a distinct type from [`Identity`]:
157 /// identical arithmetic, different invalidation.
158 fn generation(&self) -> u64 {
159 0
160 }
161
162 /// Short tag for `state_json` / diagnostics (`"mercator"`, `"layout"`, …).
163 fn kind(&self) -> &'static str;
164}
165
166/// **Identity** — the position IS the coordinate. Use case 3 (the flat 2D
167/// overlay) and any caller that already laid its own content out.
168#[derive(Clone, Debug, Default)]
169pub struct Identity {
170 pts: Vec<[f64; 2]>,
171}
172
173impl Identity {
174 #[must_use]
175 pub fn new(pts: Vec<[f64; 2]>) -> Self {
176 Self { pts }
177 }
178 /// From `f32` pairs — the shape every existing facett layout already has.
179 #[must_use]
180 pub fn from_f32(pts: &[(f32, f32)]) -> Self {
181 Self { pts: pts.iter().map(|&(x, y)| [x as f64, y as f64]).collect() }
182 }
183}
184
185impl PositionSource for Identity {
186 fn feature_count(&self) -> usize {
187 self.pts.len()
188 }
189 fn position(&self, id: u32) -> WorldPos {
190 match self.pts.get(id as usize) {
191 Some(&[x, y]) => WorldPos::flat(x, y),
192 None => WorldPos::new(f64::NAN, f64::NAN, f64::NAN),
193 }
194 }
195 fn kind(&self) -> &'static str {
196 "identity"
197 }
198}
199
200/// **Layout** — positions produced by a layout algorithm that keeps running
201/// (force-directed, hierarchical, metro). Arithmetically identical to
202/// [`Identity`]; the difference is [`generation`](PositionSource::generation),
203/// which every engine-side cache keys on.
204#[derive(Clone, Debug, Default)]
205pub struct Layout {
206 pts: Vec<[f64; 2]>,
207 epoch: u64,
208}
209
210impl Layout {
211 #[must_use]
212 pub fn new(pts: Vec<[f64; 2]>) -> Self {
213 Self { pts, epoch: 1 }
214 }
215 /// Replace the positions and bump the generation — the one mutation a
216 /// running layout performs per iteration.
217 pub fn set(&mut self, pts: Vec<[f64; 2]>) {
218 self.pts = pts;
219 self.epoch = self.epoch.wrapping_add(1);
220 }
221 pub fn set_f32(&mut self, pts: &[(f32, f32)]) {
222 self.set(pts.iter().map(|&(x, y)| [x as f64, y as f64]).collect());
223 }
224 #[must_use]
225 pub fn points(&self) -> &[[f64; 2]] {
226 &self.pts
227 }
228}
229
230impl PositionSource for Layout {
231 fn feature_count(&self) -> usize {
232 self.pts.len()
233 }
234 fn position(&self, id: u32) -> WorldPos {
235 match self.pts.get(id as usize) {
236 Some(&[x, y]) => WorldPos::flat(x, y),
237 None => WorldPos::new(f64::NAN, f64::NAN, f64::NAN),
238 }
239 }
240 fn generation(&self) -> u64 {
241 self.epoch
242 }
243 fn kind(&self) -> &'static str {
244 "layout"
245 }
246}
247
248/// **Projected** — a source that pushes raw `(a, b)` pairs through a pure
249/// function. This is the seam a geographic projection plugs into **without
250/// `facett-core` learning what a latitude is**: `facett-geomap` supplies
251/// `|lon, lat| mercator(lon, lat)` and gets a `PositionSource` back.
252///
253/// The function must be pure and cheap; it is called once per visible feature
254/// per frame (and batched through [`PositionSource::positions`]).
255pub struct Projected {
256 raw: Vec<[f64; 2]>,
257 project: Box<dyn Fn(f64, f64) -> (f64, f64) + Send + Sync>,
258 kind: &'static str,
259}
260
261impl Debug for Projected {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 f.debug_struct("Projected").field("kind", &self.kind).field("len", &self.raw.len()).finish()
264 }
265}
266
267impl Projected {
268 #[must_use]
269 pub fn new(
270 raw: Vec<[f64; 2]>,
271 kind: &'static str,
272 project: impl Fn(f64, f64) -> (f64, f64) + Send + Sync + 'static,
273 ) -> Self {
274 Self { raw, project: Box::new(project), kind }
275 }
276}
277
278impl PositionSource for Projected {
279 fn feature_count(&self) -> usize {
280 self.raw.len()
281 }
282 fn position(&self, id: u32) -> WorldPos {
283 match self.raw.get(id as usize) {
284 Some(&[a, b]) => {
285 let (x, y) = (self.project)(a, b);
286 WorldPos::flat(x, y)
287 }
288 None => WorldPos::new(f64::NAN, f64::NAN, f64::NAN),
289 }
290 }
291 fn kind(&self) -> &'static str {
292 self.kind
293 }
294}
295
296// ─────────────────────────────────────────────────────────────────────────────
297// 2. ElevationSource — terrain | flat | a metric
298// ─────────────────────────────────────────────────────────────────────────────
299
300/// **How high is feature `id`?**
301///
302/// `facett-map3d` is, in its entirety, one implementation of this trait: a 3D
303/// map is a 2D map whose elevation is a heightfield instead of `0.0`. A graph in
304/// 3D is the same statement with centrality or risk on the Z axis.
305pub trait ElevationSource: Debug {
306 /// Height of feature `id`, given the world position the [`PositionSource`]
307 /// produced (a terrain source samples at `p`; a metric source ignores `p`).
308 fn elevation(&self, id: u32, p: WorldPos) -> f64;
309
310 /// The `[min, max]` this source can ever emit — the culler needs it to grow
311 /// 2D bounds into 3D ones without sampling every feature.
312 fn z_range(&self) -> (f64, f64) {
313 (0.0, 0.0)
314 }
315
316 /// Batch form; default loops.
317 fn elevations(&self, ids: &[u32], pos: &[WorldPos], out: &mut Vec<f64>) {
318 out.clear();
319 out.reserve(ids.len());
320 for (n, &id) in ids.iter().enumerate() {
321 out.push(self.elevation(id, pos.get(n).copied().unwrap_or_default()));
322 }
323 }
324
325 fn kind(&self) -> &'static str;
326}
327
328/// **Flat** — `Z = 0`. Use cases 1, 3 and 4. Zero cost: the engine special-cases
329/// a `z_range()` of `(0, 0)` and skips the elevation pass entirely.
330#[derive(Clone, Copy, Debug, Default)]
331pub struct Flat;
332
333impl ElevationSource for Flat {
334 fn elevation(&self, _id: u32, _p: WorldPos) -> f64 {
335 0.0
336 }
337 fn elevations(&self, ids: &[u32], _pos: &[WorldPos], out: &mut Vec<f64>) {
338 out.clear();
339 out.resize(ids.len(), 0.0);
340 }
341 fn kind(&self) -> &'static str {
342 "flat"
343 }
344}
345
346/// **Metric** — per-feature scalar × a scale. Use cases 5 and 6: node height IS
347/// betweenness centrality, or a Mímir risk score.
348#[derive(Clone, Debug, Default)]
349pub struct Metric {
350 values: Vec<f32>,
351 scale: f64,
352 lo: f64,
353 hi: f64,
354}
355
356impl Metric {
357 #[must_use]
358 pub fn new(values: Vec<f32>, scale: f64) -> Self {
359 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
360 for &v in &values {
361 let z = v as f64 * scale;
362 if z.is_finite() {
363 lo = lo.min(z);
364 hi = hi.max(z);
365 }
366 }
367 if !lo.is_finite() {
368 lo = 0.0;
369 hi = 0.0;
370 }
371 Self { values, scale, lo, hi }
372 }
373}
374
375impl ElevationSource for Metric {
376 fn elevation(&self, id: u32, _p: WorldPos) -> f64 {
377 self.values.get(id as usize).map_or(0.0, |&v| v as f64 * self.scale)
378 }
379 fn z_range(&self) -> (f64, f64) {
380 (self.lo, self.hi)
381 }
382 fn kind(&self) -> &'static str {
383 "metric"
384 }
385}
386
387/// **Sampled** — height from a pure `(x, y) -> z` function plus a declared
388/// range. `facett-geomap` wraps its terrain heightfield in this; `facett-core`
389/// never learns what a DEM is.
390pub struct Sampled {
391 sample: Box<dyn Fn(f64, f64) -> f64 + Send + Sync>,
392 range: (f64, f64),
393 kind: &'static str,
394}
395
396impl Debug for Sampled {
397 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398 f.debug_struct("Sampled").field("kind", &self.kind).field("range", &self.range).finish()
399 }
400}
401
402impl Sampled {
403 #[must_use]
404 pub fn new(
405 kind: &'static str,
406 range: (f64, f64),
407 sample: impl Fn(f64, f64) -> f64 + Send + Sync + 'static,
408 ) -> Self {
409 Self { sample: Box::new(sample), range, kind }
410 }
411}
412
413impl ElevationSource for Sampled {
414 fn elevation(&self, _id: u32, p: WorldPos) -> f64 {
415 (self.sample)(p.x, p.y)
416 }
417 fn z_range(&self) -> (f64, f64) {
418 self.range
419 }
420 fn kind(&self) -> &'static str {
421 self.kind
422 }
423}
424
425// ─────────────────────────────────────────────────────────────────────────────
426// 3. Hierarchy — what "one step deeper" means
427// ─────────────────────────────────────────────────────────────────────────────
428
429/// **What does one step deeper mean?**
430///
431/// ★ The key idea of the whole engine: *a map zoom and a graph cluster-expand are
432/// the same operation* — descend one level in a hierarchy of bounding volumes and
433/// swap coarse instances for fine ones. Both are this trait. There is one culler
434/// and one indirect draw serving both.
435///
436/// Level `0` is the **coarsest** (Sweden; the repo node). `depth() - 1` is the
437/// finest (a street; a call edge).
438pub trait Hierarchy: Debug {
439 /// Number of levels, `>= 1`.
440 fn depth(&self) -> u32;
441
442 /// Which level this camera scale wants. `scale` is world-units-per-pixel's
443 /// reciprocal — the same number `Facet::scale_range()` clamps, so a map band
444 /// and a graph band are literally the same control.
445 fn level_for(&self, scale: f32) -> u32;
446
447 /// Every feature present at `level`, ascending. For a map this is "the tiles
448 /// / features of this zoom band"; for a graph, "the visible nodes with the
449 /// currently-expanded clusters substituted in".
450 fn members(&self, level: u32) -> &[u32];
451
452 /// The children that replace `parent` when you descend from `level` to
453 /// `level + 1`. Empty = `parent` is a leaf and survives the descent.
454 fn expand(&self, level: u32, parent: u32) -> &[u32];
455
456 /// The inverse of [`expand`](Self::expand): the coarse feature that stands in
457 /// for `child` at `level - 1`. Drives "zoom out and my selection follows".
458 fn parent_of(&self, level: u32, child: u32) -> Option<u32>;
459
460 fn kind(&self) -> &'static str;
461}
462
463/// **Flatten** — one level, everything in it. The honest hierarchy of a small
464/// graph or a single-zoom overlay; also the migration floor, so a renderer can
465/// move onto the engine before it has a real LOD story.
466#[derive(Clone, Debug, Default)]
467pub struct Flatten {
468 all: Vec<u32>,
469}
470
471impl Flatten {
472 #[must_use]
473 pub fn new(n: usize) -> Self {
474 Self { all: (0..n as u32).collect() }
475 }
476}
477
478impl Hierarchy for Flatten {
479 fn depth(&self) -> u32 {
480 1
481 }
482 fn level_for(&self, _scale: f32) -> u32 {
483 0
484 }
485 fn members(&self, _level: u32) -> &[u32] {
486 &self.all
487 }
488 fn expand(&self, _level: u32, _parent: u32) -> &[u32] {
489 &[]
490 }
491 fn parent_of(&self, _level: u32, _child: u32) -> Option<u32> {
492 None
493 }
494 fn kind(&self) -> &'static str {
495 "flatten"
496 }
497}
498
499/// One rung of a [`Bands`] hierarchy.
500#[derive(Clone, Debug, Default)]
501pub struct Band {
502 /// The features drawn at this level.
503 pub members: Vec<u32>,
504 /// `scale >= min_scale` selects this level (levels are searched coarse→fine,
505 /// so the finest band whose threshold is met wins).
506 pub min_scale: f32,
507 /// `parent -> children` for the descent into the next level.
508 pub children: std::collections::BTreeMap<u32, Vec<u32>>,
509}
510
511/// **Bands** — THE shared hierarchy. A map zoom pyramid and a graph
512/// cluster-expansion tree are both a list of [`Band`]s; nothing in the engine
513/// distinguishes them.
514#[derive(Clone, Debug, Default)]
515pub struct Bands {
516 levels: Vec<Band>,
517 /// `level -> (child -> parent)`, derived once from `children`.
518 parents: Vec<std::collections::BTreeMap<u32, u32>>,
519 kind: &'static str,
520}
521
522impl Bands {
523 #[must_use]
524 pub fn new(levels: Vec<Band>, kind: &'static str) -> Self {
525 let parents = levels
526 .iter()
527 .map(|b| {
528 let mut m = std::collections::BTreeMap::new();
529 for (&p, cs) in &b.children {
530 for &c in cs {
531 m.insert(c, p);
532 }
533 }
534 m
535 })
536 .collect();
537 Self { levels, parents, kind }
538 }
539}
540
541impl Hierarchy for Bands {
542 fn depth(&self) -> u32 {
543 self.levels.len().max(1) as u32
544 }
545 fn level_for(&self, scale: f32) -> u32 {
546 let mut chosen = 0u32;
547 for (i, b) in self.levels.iter().enumerate() {
548 if scale >= b.min_scale {
549 chosen = i as u32;
550 }
551 }
552 chosen
553 }
554 fn members(&self, level: u32) -> &[u32] {
555 self.levels.get(level as usize).map_or(&[], |b| &b.members)
556 }
557 fn expand(&self, level: u32, parent: u32) -> &[u32] {
558 self.levels
559 .get(level as usize)
560 .and_then(|b| b.children.get(&parent))
561 .map_or(&[], |v| v.as_slice())
562 }
563 fn parent_of(&self, level: u32, child: u32) -> Option<u32> {
564 let l = level.checked_sub(1)?;
565 self.parents.get(l as usize).and_then(|m| m.get(&child)).copied()
566 }
567 fn kind(&self) -> &'static str {
568 self.kind
569 }
570}