facett-core/src/render/layer.rs
//! **The graphs-over-maps seam** — a z-ordered [`LayerStack`].
//!
//! The render doctrine's "graphs-overlaid-on-maps = LATER" line is unblocked by
//! adding the *seam* now (not the feature): a shared camera (see
//! [`super::camera::Camera`]) plus a z-ordered stack of [`Layer`]s so a graph skin
//! can later composite over a map skin without a retrofit. This milestone ships
//! the struct + its z-order contract only — **no consumer is wired**; the skins
//! still draw themselves directly.
/// What a layer *is* — the domain kind drawn at this z. Opaque to the kernel: the
/// stack only orders layers; it never interprets the kind. Extend as skins land.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LayerKind {
/// A 2D vector basemap (facett-map).
Map2d,
/// A 3D extruded map (facett-map3d).
Map3d,
/// A graph drawn over the base (facett-graphview / its skins).
Graph,
/// A caller-supplied overlay (decorations, ravens, HUD).
Overlay,
}
/// One stackable layer: its paint **z** (lower draws first / underneath) and its
/// [`LayerKind`]. The renderer composites layers in ascending z.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Layer {
/// Paint order key — ascending z paints back→front (a higher-z graph layer
/// composites *over* a lower-z map layer). Ties keep insertion order (stable).
pub z: f32,
pub kind: LayerKind,
}
impl Layer {
pub fn new(z: f32, kind: LayerKind) -> Self {
Self { z, kind }
}
}
/// A z-ordered set of [`Layer`]s — the composition seam for graphs-over-maps. Push
/// layers in any order; [`LayerStack::ordered`] yields them back→front (ascending
/// z), with insertion order breaking ties so the result is deterministic.
#[derive(Clone, Debug, Default)]
pub struct LayerStack {
layers: Vec<Layer>,
}
impl LayerStack {
pub fn new() -> Self {
Self::default()
}
/// Push a layer; returns its insertion index (its tie-break key).
pub fn push(&mut self, layer: Layer) -> usize {
self.layers.push(layer);
self.layers.len() - 1
}
/// Convenience: push `kind` at `z`.
pub fn add(&mut self, z: f32, kind: LayerKind) -> usize {
self.push(Layer::new(z, kind))
}
pub fn len(&self) -> usize {
self.layers.len()
}
pub fn is_empty(&self) -> bool {
self.layers.is_empty()
}
/// The layers in **paint order** — ascending z, insertion order breaking ties.
/// Back→front: the renderer draws element 0 first (underneath), the last on top.
pub fn ordered(&self) -> Vec<Layer> {
let mut idx: Vec<usize> = (0..self.layers.len()).collect();
// Stable sort on z keeps insertion order for equal z (the tie-break rule).
idx.sort_by(|&a, &b| {
self.layers[a].z.partial_cmp(&self.layers[b].z).unwrap_or(std::cmp::Ordering::Equal)
});
idx.into_iter().map(|i| self.layers[i]).collect()
}
/// The topmost layer (largest z; last-inserted wins a tie), or `None` if empty.
pub fn top(&self) -> Option<Layer> {
self.ordered().last().copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// INJECT-ASSERT: layers come back ascending-z regardless of push order, so a
/// graph pushed under (higher z) a map composites on top.
#[test]
fn layer_stack_orders_back_to_front_by_z() {
let mut s = LayerStack::new();
s.add(10.0, LayerKind::Graph); // pushed first, but highest z
s.add(0.0, LayerKind::Map3d); // base
s.add(5.0, LayerKind::Map2d);
let order = s.ordered();
assert_eq!(order.len(), 3);
assert_eq!(order[0].kind, LayerKind::Map3d, "lowest z drawn first (underneath)");
assert_eq!(order[1].kind, LayerKind::Map2d);
assert_eq!(order[2].kind, LayerKind::Graph, "highest z composites on top");
assert_eq!(s.top().unwrap().kind, LayerKind::Graph);
}
/// INJECT-ASSERT: equal z keeps insertion order (stable tie-break) — two
/// overlays at the same z paint in the order the host added them.
#[test]
fn layer_stack_breaks_ties_by_insertion_order() {
let mut s = LayerStack::new();
s.add(1.0, LayerKind::Map2d);
let first = s.add(2.0, LayerKind::Overlay);
let second = s.add(2.0, LayerKind::Graph);
let _ = (first, second);
let order = s.ordered();
assert_eq!(order[0].kind, LayerKind::Map2d);
assert_eq!(order[1].kind, LayerKind::Overlay, "first-inserted of the tie paints first");
assert_eq!(order[2].kind, LayerKind::Graph);
}
#[test]
fn empty_stack_has_no_top() {
let s = LayerStack::new();
assert!(s.is_empty());
assert!(s.top().is_none());
}
}