Skip to main content

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}