facett-demo/src/data.rs
//! **The one fake dataset.** A small, deterministic "fleet / issues" world that
//! every facett component renders a different facet of:
//!
//! - the **fleet** (vessels: region, status, score, progress, trend, last-seen
//! position) → an Arrow [`RecordBatch`] feeding the **grid** + **table**, plus
//! a flagship **card**/**form** for one vessel and **pop** progress rows.
//! - the **service dependency graph** (which subsystem feeds which) → the
//! **graph**, **graph3d**, **depgraph** and **pipeline** views.
//! - **telemetry series** (throughput over time per region) → **plot** (line/bars)
//! and the **plotters** family (box / violin / heatmap / candlestick / treemap).
//! - the vessels' **last-seen lon/lat** → the **map** (and the **osm** adapter
//! shares `MapView`, exercised here via the same point cloud).
//!
//! All numbers are generated from cheap deterministic formulae (no RNG, no
//! external files) so the self-check test can assert exact counts/values.
use std::sync::Arc;
use arrow_array::{BooleanArray, Date32Array, Float64Array, Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};
/// How many vessels in the fake fleet. Small enough to read, big enough that the
/// grid virtualization, enum-chip detection and sparklines all kick in.
pub const FLEET: usize = 48;
/// The four operating regions (low-cardinality → grid renders them as chips).
pub const REGIONS: [&str; 4] = ["north", "south", "east", "west"];
/// The lifecycle status of a vessel (also low-cardinality → chips).
pub const STATUSES: [&str; 4] = ["patrol", "docked", "transit", "alert"];
/// A few crew/assignees so the card's `User` field has names.
pub const CREW: [&str; 4] = ["Astrid", "Bjørn", "Senna", "Ravn"];
/// One vessel row — the canonical record the whole demo is built from.
#[derive(Clone, Debug)]
pub struct Vessel {
pub id: i64,
pub name: String,
pub region: &'static str,
pub status: &'static str,
pub crew: &'static str,
/// Health score 0..100.
pub score: f64,
/// Mission progress 0.0..1.0.
pub progress: f64,
/// A 7-point recent trend (feeds the grid sparkline + telemetry).
pub trend: Vec<f64>,
pub active: bool,
/// Days since the unix epoch (Arrow `Date32`) for the "opened" column.
pub opened: i32,
/// Last-seen position.
pub lon: f64,
pub lat: f64,
}
/// Build the deterministic fleet. Pure: same vessels every run.
pub fn fleet() -> Vec<Vessel> {
(0..FLEET)
.map(|i| {
let id = 1000 + i as i64 * 137;
// A trend window: a sine wave with a per-vessel phase so each row's
// sparkline differs.
let trend: Vec<f64> =
(0..7).map(|k| ((i as f64 * 0.6 + k as f64 * 0.9).sin() * 40.0 + 55.0).round()).collect();
Vessel {
id,
name: format!("vessel-{i:03}"),
region: REGIONS[i % REGIONS.len()],
status: STATUSES[(i * 3 + 1) % STATUSES.len()],
crew: CREW[(i * 7) % CREW.len()],
score: ((i * 41 + 7) % 100) as f64,
progress: ((i * 17) % 100) as f64 / 100.0,
trend,
active: i % 3 != 0,
opened: 18628 + (i as i32 * 7), // counting from 2021-01-01
// Spread points across the Nordic seas (lon 4..28, lat 54..69).
lon: 4.0 + (i as f64 * 0.5).sin() * 12.0 + 12.0,
lat: 54.0 + ((i as f64 * 0.37).cos() * 0.5 + 0.5) * 15.0,
}
})
.collect()
}
/// Materialise the fleet into an Arrow [`RecordBatch`] — built **once** and fed to
/// both the grid (`Grid::from_record_batch`) and the table
/// (`facett::arrow::table_from_batch`), per the D5 brief.
pub fn fleet_batch(fleet: &[Vessel]) -> RecordBatch {
let id: Vec<i64> = fleet.iter().map(|v| v.id).collect();
let name: Vec<String> = fleet.iter().map(|v| v.name.clone()).collect();
let region: Vec<&str> = fleet.iter().map(|v| v.region).collect();
let status: Vec<&str> = fleet.iter().map(|v| v.status).collect();
let score: Vec<f64> = fleet.iter().map(|v| v.score).collect();
let progress: Vec<f64> = fleet.iter().map(|v| v.progress).collect();
// The sparkline column samples the last trend point so the grid still has a
// single numeric per row; the per-row "trend" sparkline reads this column's
// window across rows (that's how facett-grid's sparkline cell works).
let trend: Vec<f64> = fleet.iter().map(|v| *v.trend.last().unwrap_or(&0.0)).collect();
let active: Vec<bool> = fleet.iter().map(|v| v.active).collect();
let opened: Vec<i32> = fleet.iter().map(|v| v.opened).collect();
RecordBatch::try_new(
Arc::new(Schema::new(vec![
Field::new("id", DataType::Int64, false),
Field::new("name", DataType::Utf8, false),
Field::new("region", DataType::Utf8, false),
Field::new("status", DataType::Utf8, false),
Field::new("score", DataType::Float64, false),
Field::new("progress", DataType::Float64, false),
Field::new("trend", DataType::Float64, false),
Field::new("active", DataType::Boolean, false),
Field::new("opened", DataType::Date32, false),
])),
vec![
Arc::new(Int64Array::from(id)),
Arc::new(StringArray::from(name)),
Arc::new(StringArray::from(region)),
Arc::new(StringArray::from(status)),
Arc::new(Float64Array::from(score)),
Arc::new(Float64Array::from(progress)),
Arc::new(Float64Array::from(trend)),
Arc::new(BooleanArray::from(active)),
Arc::new(Date32Array::from(opened)),
],
)
.expect("fleet batch columns are equal length")
}
/// The service subsystems that make up a vessel's stack — the node set for the
/// graph / graph3d / depgraph / pipeline views. `(label, sublabel)`.
pub const SUBSYSTEMS: [(&str, &str); 8] = [
("Ingest", "AIS feed"),
("Decode", "WKB → geo"),
("Fuse", "track merge"),
("Score", "risk model"),
("Index", "redb store"),
("Map", "tile render"),
("Alert", "dispatch"),
("Audit", "trail log"),
];
/// Directed dependencies between [`SUBSYSTEMS`] (indices): a small DAG.
pub const DEPS: [(usize, usize); 9] =
[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (4, 6), (3, 6), (6, 7), (5, 7)];
/// The geo point cloud (lon, lat) — the vessels' last-seen positions, shared by
/// `map` and (conceptually) the `osm` adapter, which produces the same `MapView`.
pub fn geo_points(fleet: &[Vessel]) -> Vec<(f64, f64)> {
fleet.iter().map(|v| (v.lon, v.lat)).collect()
}
/// One [`geo_ways`] row: `(osm_id, tags, lonlat-vertices)`.
pub type GeoWay = (i64, Vec<(String, String)>, Vec<(f64, f64)>);
/// A small **synthetic vector basemap** for the `map` tab — roads, a coastline,
/// and a water body around the fleet's centroid, so the showcase renders a real
/// basemap (lines, not just dots) the way korp's Map will from `ways.parquet`.
/// Returns `(osm_id, tags, lonlat-vertices)` rows; the demo turns each into a
/// `facett::map::Way` via `Way::from_lonlat`. Deterministic.
pub fn geo_ways(fleet: &[Vessel]) -> Vec<GeoWay> {
let n = fleet.len().max(1) as f64;
let clon = fleet.iter().map(|v| v.lon).sum::<f64>() / n;
let clat = fleet.iter().map(|v| v.lat).sum::<f64>() / n;
let tag = |k: &str, v: &str| vec![(k.to_string(), v.to_string())];
let mut out: Vec<GeoWay> = Vec::new();
// A grid of roads (motorway/secondary verticals + residential horizontals).
for i in 0..18 {
let f = i as f64 / 18.0 - 0.5; // -0.5..0.5
out.push((
i,
tag("highway", if i % 6 == 0 { "motorway" } else { "secondary" }),
(0..10).map(|j| (clon + f * 6.0, clat - 3.0 + j as f64 * 0.6)).collect(),
));
out.push((
100 + i,
tag("highway", "residential"),
(0..10).map(|j| (clon - 3.0 + j as f64 * 0.6, clat + f * 6.0)).collect(),
));
}
// A coastline (a long wavy line) + a water polygon (a lake).
out.push((
900,
tag("natural", "coastline"),
(0..40).map(|j| (clon - 4.0 + j as f64 * 0.2, clat + 2.5 + (j as f64 * 0.5).sin() * 0.4)).collect(),
));
out.push((
901,
tag("natural", "water"),
vec![
(clon + 1.0, clat - 1.0),
(clon + 2.0, clat - 1.0),
(clon + 2.0, clat - 0.2),
(clon + 1.0, clat - 0.2),
(clon + 1.0, clat - 1.0),
],
));
out
}
/// The **synthetic test basemap** for the `map` tab: the **znippy rabbit** logo,
/// not a square grid or terrain. The parametric rabbit
/// ([`facett_core::rabbit`]) lives in a normalised `[-1, 1]²` design box (y up);
/// we place it lon/lat-centred on the fleet's centroid and scale it to a few
/// degrees, then turn each silhouette loop into a **filled** [`facett::map::Way`]
/// (the solid mascot) plus the eye as a small dark filled dot. Deterministic — no
/// RNG, so the snapshot is stable. This is *only* the synthetic fallback; the
/// real-Sweden path (loading `ways.parquet`) is untouched and bypasses this.
pub fn rabbit_ways(fleet: &[Vessel]) -> Vec<facett::map::Way> {
use facett::map::Way;
let n = fleet.len().max(1) as f64;
let clon = fleet.iter().map(|v| v.lon).sum::<f64>() / n;
let clat = fleet.iter().map(|v| v.lat).sum::<f64>() / n;
// Scale the [-1,1] box to ~±5° lon / ±4° lat around the centroid. Mercator
// squashes latitude a touch, so use a slightly larger lat span to keep the
// rabbit upright rather than squat.
let sx = 5.0;
let sy = 4.6;
let to_lonlat = |(x, y): (f64, f64)| (clon + x * sx, clat + y * sy);
// Facett palette colours: a warm-grey fur fill + a crisp dark outline + a
// dark eye. These read on any of the demo themes (the silhouette is a solid
// mass with a high-contrast outline).
let fur = egui::Color32::from_rgb(196, 201, 214);
let outline = egui::Color32::from_rgb(28, 32, 44);
let eye = egui::Color32::from_rgb(20, 22, 30);
let r = facett::core::rabbit::Rabbit::default();
let mut ways: Vec<Way> = Vec::new();
// body, head, ear, ear — each a filled silhouette loop with the outline on top.
let kinds = ["body", "head", "ear", "ear"];
for (i, lp) in r.silhouette().into_iter().enumerate() {
let lonlat: Vec<(f64, f64)> = lp.into_iter().map(to_lonlat).collect();
let kind = kinds.get(i).copied().unwrap_or("body");
ways.push(Way::filled(i as i64, kind, lonlat, fur, outline, 1.6));
}
// The eye: a small dark filled dot on top of the head.
let eye_loop: Vec<(f64, f64)> = r.eye().into_iter().map(to_lonlat).collect();
ways.push(Way::filled(900, "eye", eye_loop, eye, eye, 0.8));
ways
}
/// Per-region throughput time-series for the `plot` (line + bars) — one named
/// series per region, 24 hourly samples each.
pub fn telemetry() -> Vec<(String, Vec<(f64, f64)>)> {
REGIONS
.iter()
.enumerate()
.map(|(r, region)| {
let pts: Vec<(f64, f64)> = (0..24)
.map(|h| {
let y = ((h as f64 * 0.4 + r as f64).sin() * 30.0 + 60.0 + r as f64 * 8.0).max(0.0);
(h as f64, y)
})
.collect();
(region.to_string(), pts)
})
.collect()
}
/// Grouped score samples per region — feeds the box plot + violin (distribution
/// views in `facett-plotters`).
pub fn score_groups(fleet: &[Vessel]) -> Vec<(String, Vec<f64>)> {
REGIONS
.iter()
.map(|region| {
let vals: Vec<f64> = fleet.iter().filter(|v| &v.region == region).map(|v| v.score).collect();
(region.to_string(), vals)
})
.collect()
}
/// A region × status count matrix for the heatmap (rows = regions, cols =
/// statuses). Returns `(rows, cols, row_labels, col_labels, data)`.
pub fn region_status_matrix(fleet: &[Vessel]) -> (usize, usize, Vec<String>, Vec<String>, Vec<f64>) {
let rows = REGIONS.len();
let cols = STATUSES.len();
let mut data = vec![0.0f64; rows * cols];
for v in fleet {
let r = REGIONS.iter().position(|&x| x == v.region).unwrap();
let c = STATUSES.iter().position(|&x| x == v.status).unwrap();
data[r * cols + c] += 1.0;
}
(
rows,
cols,
REGIONS.iter().map(|s| s.to_string()).collect(),
STATUSES.iter().map(|s| s.to_string()).collect(),
data,
)
}
/// Total "tonnage" (fake) per region for the treemap — area-weighted tiles.
pub fn region_tonnage(fleet: &[Vessel]) -> Vec<(String, f64)> {
REGIONS
.iter()
.map(|region| {
let t: f64 = fleet.iter().filter(|v| &v.region == region).map(|v| v.score + 20.0).sum();
(region.to_string(), t)
})
.collect()
}
/// Density **hotspots** (lon, lat, count, label) for the scrollable/zoomable
/// `geomap` — the densest regions of the fleet, drawn as count-sized circles over
/// the point cloud. Deterministic: the four region centroids, weighted by how many
/// vessels sit in each region.
pub fn geo_hotspots(fleet: &[Vessel]) -> Vec<(f64, f64, usize, String)> {
REGIONS
.iter()
.map(|region| {
let pts: Vec<&Vessel> = fleet.iter().filter(|v| &v.region == region).collect();
let n = pts.len().max(1);
let lon = pts.iter().map(|v| v.lon).sum::<f64>() / n as f64;
let lat = pts.iter().map(|v| v.lat).sum::<f64>() / n as f64;
(lon, lat, pts.len(), region.to_string())
})
.collect()
}
/// Bin a real `(lon, lat)` point cloud into the **`n` densest cells** and return
/// them as `(lon, lat, count, label)` **hotspots** for the `geomap` — the count is
/// the number of points in the cell, the position its centroid. Used when the demo
/// loads real OSM points (Sigtuna): the geomap then shows where the map is dense
/// (town centre vs. fields) as count-sized circles over the dots, instead of the
/// four fake fleet-region hotspots. Deterministic (a fixed grid, stable sort).
pub fn density_hotspots(points: &[(f64, f64)], n: usize) -> Vec<(f64, f64, usize, String)> {
if points.is_empty() || n == 0 {
return Vec::new();
}
// A coarse ~grid over the cloud's bounds (≈24×24 cells) so popular cells stand
// out. Quantise to integer cell keys, accumulate count + centroid.
let (mut min_lon, mut max_lon) = (f64::MAX, f64::MIN);
let (mut min_lat, mut max_lat) = (f64::MAX, f64::MIN);
for &(lon, lat) in points {
min_lon = min_lon.min(lon);
max_lon = max_lon.max(lon);
min_lat = min_lat.min(lat);
max_lat = max_lat.max(lat);
}
const CELLS: f64 = 24.0;
let span_lon = (max_lon - min_lon).max(1e-9);
let span_lat = (max_lat - min_lat).max(1e-9);
use std::collections::HashMap;
// key → (count, sum_lon, sum_lat)
let mut bins: HashMap<(i64, i64), (usize, f64, f64)> = HashMap::new();
for &(lon, lat) in points {
let cx = (((lon - min_lon) / span_lon) * CELLS).floor() as i64;
let cy = (((lat - min_lat) / span_lat) * CELLS).floor() as i64;
let e = bins.entry((cx, cy)).or_insert((0, 0.0, 0.0));
e.0 += 1;
e.1 += lon;
e.2 += lat;
}
let mut cells: Vec<(f64, f64, usize)> =
bins.into_iter().map(|(_, (c, sl, sa))| (sl / c as f64, sa / c as f64, c)).collect();
// Densest first; break ties by position so the result is deterministic.
cells.sort_by(|a, b| b.2.cmp(&a.2).then(a.0.partial_cmp(&b.0).unwrap()).then(a.1.partial_cmp(&b.1).unwrap()));
cells
.into_iter()
.take(n)
.map(|(lon, lat, count)| (lon, lat, count, format!("{count} pts")))
.collect()
}
/// A handful of **system nodes** (id, label, badge, normalized pos) for the
/// `syschart` system-map — the same subsystem set as the graph family, laid out as
/// free-positioned peers (not a layered DAG) with an event-count badge each. The
/// badge counts the vessels whose status routes through that subsystem (a cheap
/// deterministic signal so the chart's badges are non-zero).
pub fn sys_nodes(fleet: &[Vessel]) -> Vec<(&'static str, &'static str, u64, (f32, f32))> {
// Deterministic ring layout: evenly spaced around the canvas.
let n = SUBSYSTEMS.len();
SUBSYSTEMS
.iter()
.enumerate()
.map(|(i, (label, _))| {
let theta = std::f32::consts::TAU * (i as f32) / (n as f32);
let pos = (0.5 + 0.38 * theta.cos(), 0.5 + 0.38 * theta.sin());
// A non-zero badge: vessels whose id mod the subsystem count lands here.
let badge = fleet.iter().filter(|v| (v.id as usize) % n == i).count() as u64;
(*label, SUBSYSTEMS[i].1, badge, pos)
})
.collect()
}
/// Undirected `syschart` links (id pairs) derived from the directed [`DEPS`] DAG.
pub fn sys_edges() -> Vec<(&'static str, &'static str)> {
DEPS.iter().map(|&(a, b)| (SUBSYSTEMS[a].0, SUBSYSTEMS[b].0)).collect()
}
/// Two **Iceberg snapshots** of the fleet table for the `icebergview` time-travel
/// browser: an older `v1` (the first half of the fleet) and a newer `v2` (the whole
/// fleet). Returns `[(snapshot_id, timestamp_ms, RecordBatch)]`, oldest first (the
/// view sorts newest-first itself). Selecting v1 vs v2 changes the visible rows —
/// real time travel the robot can drive + assert.
pub fn iceberg_snapshots(fleet: &[Vessel]) -> Vec<(i64, i64, RecordBatch)> {
let half = fleet.len() / 2;
let v1 = fleet_batch(&fleet[..half]);
let v2 = fleet_batch(fleet);
vec![(100, 1_700_000_000_000, v1), (200, 1_700_000_086_400, v2)]
}
/// A short OHLC candle series (fake "alert price" over 12 sessions).
pub fn candles() -> Vec<(f64, f64, f64, f64)> {
(0..12)
.map(|i| {
let base = (i as f64 * 0.5).sin() * 8.0 + 40.0;
let open = base;
let close = base + (i as f64 * 0.9).cos() * 4.0;
let high = open.max(close) + 2.0;
let low = open.min(close) - 2.0;
(open, high, low, close)
})
.collect()
}
/// A small **bundled sample manual** for the `docview` tab: a few synthetic
/// "Typst-exported" SVG pages (one SVG per page), each a white A4-ish page with a
/// coloured title bar carrying the **chapter title**, a few **body paragraph
/// lines**, and a **page-number footer** — all as real SVG `<text>`. The `<text>`
/// is what proves the docview pipeline: facett-docview embeds DejaVu Sans into its
/// usvg `fontdb`, so these glyphs shape + rasterize even with **zero system fonts**
/// (native airgap AND the browser/wasm path, where there are none). Earlier this
/// page was pure `<rect>`/`<line>` "stand-in" blocks, so the demo only ever showed
/// boxes — never exercising glyph rendering. Returns `(svg_bytes_per_page, toc)`
/// where the TOC is the `(title, page_index)` list the sidebar shows. Deterministic
/// (no RNG/files) so the self-check test can assert exact page/TOC counts and that
/// every page carries `<text>`.
pub fn sample_manual() -> (Vec<Vec<u8>>, Vec<(String, usize)>) {
// Page geometry in pt (a portrait page ≈ 1:1.4).
const W: u32 = 420;
const H: u32 = 594;
let chapters = ["Welcome", "Components", "Theming", "Headless tests"];
// A distinct title-bar tint per page so each rasterized page looks different.
let tints = ["#1e3a5f", "#3a5f1e", "#5f1e3a", "#5f4a1e"];
// A couple of body lines per chapter — plain ASCII so any open sans shapes them.
let bodies = [
[
"facett is one egui look across windows, macos and device.",
"Every component reads the published palette, so a preset",
"switch re-themes the whole deck coherently (the COH-1 law).",
],
[
"Graphs, grids, maps, plots, cards and forms compose freely.",
"The flagship Arrow grid stays O(visible) at a million rows,",
"and typed copy/paste moves columns zero-copy between facets.",
],
[
"Colours are authored in OKLCH semantic roles, not literals.",
"egui widget states derive by lightness steps, and the WCAG",
"gate keeps body text at 4.5:1 across light and dark presets.",
],
[
"The whole demo is headless-constructible, so a robot can",
"drive exactly what a human sees. This very page renders as",
"real glyphs from an embedded font, even with no system fonts.",
],
];
let pages: Vec<Vec<u8>> = chapters
.iter()
.enumerate()
.map(|(i, title)| {
let tint = tints[i % tints.len()];
let body = &bodies[i % bodies.len()];
// A real "page": white ground, a coloured title bar with the chapter
// title as `<text>`, three body lines as `<text>`, a footer rule and a
// page-number `<text>`. The glyphs shape against facett-docview's
// embedded DejaVu Sans — proof the docview font path works headless.
format!(
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">
<rect x="0" y="0" width="{W}" height="{H}" fill="white"/>
<rect x="0" y="0" width="{W}" height="64" fill="{tint}"/>
<text x="32" y="40" font-family="DejaVu Sans, sans-serif" font-size="22" font-weight="bold" fill="white">{title}</text>
<text x="32" y="120" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#222222">{b0}</text>
<text x="32" y="144" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#444444">{b1}</text>
<text x="32" y="168" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#444444">{b2}</text>
<line x1="32" y1="{fy}" x2="388" y2="{fy}" stroke="#999999" stroke-width="1"/>
<text x="{px}" y="{py}" font-family="DejaVu Sans, sans-serif" font-size="11" fill="{tint}" text-anchor="middle">Page {pageno}</text>
</svg>"##,
b0 = body[0],
b1 = body[1],
b2 = body[2],
fy = H - 48,
px = W / 2,
py = H - 30,
pageno = i + 1,
)
.into_bytes()
})
.collect();
let toc = chapters.iter().enumerate().map(|(i, c)| (c.to_string(), i)).collect();
(pages, toc)
}
#[cfg(test)]
mod hotspot_tests {
use super::density_hotspots;
/// `density_hotspots` returns the densest cells first, with counts + centroids:
/// a tight cluster of many points outweighs a lone outlier, and the result is
/// deterministic + capped at `n`.
#[test]
fn density_hotspots_ranks_dense_cells_first() {
// 100 points jammed into one tiny cell + a single far outlier.
let mut pts: Vec<(f64, f64)> = (0..100).map(|i| (17.60 + (i as f64) * 1e-5, 59.62 + (i as f64) * 1e-5)).collect();
pts.push((17.85, 59.68)); // the lone outlier
let hs = density_hotspots(&pts, 3);
assert!(!hs.is_empty(), "produced hotspots");
assert!(hs.len() <= 3, "capped at n");
// The densest hotspot is the big cluster (count 100), centred near it.
let (lon, lat, count, label) = &hs[0];
assert_eq!(*count, 100, "the dense cell carries all 100 points");
assert!((*lon - 17.60).abs() < 0.01 && (*lat - 59.62).abs() < 0.01, "centroid near the cluster");
assert_eq!(label, "100 pts");
// Deterministic: same input → same output.
assert_eq!(density_hotspots(&pts, 3), hs);
// Empty input is graceful.
assert!(density_hotspots(&[], 3).is_empty());
}
}
#[cfg(test)]
mod docview_tests {
use super::sample_manual;
use facett::docview::DocPage;
/// Count opaque dark pixels — the rendered glyph ink. Body text is dark
/// (#222/#444) on a white page, so real text rasterizes as dark ink; the old
/// pure-rect page would still have *some* dark rects, so we render text-only
/// regions implicitly by demanding a healthy ink count from the glyph lines.
fn dark_ink(img: &egui::ColorImage) -> usize {
img.pixels
.iter()
.filter(|c| c.a() == 255 && c.r() < 128 && c.g() < 128 && c.b() < 128)
.count()
}
/// REGRESSION (docview shows boxes, not text): every `sample_manual` page must
/// carry real `<text>` AND rasterize visible glyph ink through facett-docview's
/// embedded font — with NO reliance on system fonts (the wasm/airgap floor).
/// The old sample pages were pure `<rect>`/`<line>` stand-ins, so the demo only
/// ever showed boxes. This drives the actual docview parse+rasterize path the
/// demo uses and asserts each page parses, keeps `<text>`, and paints ink.
#[test]
fn sample_manual_pages_render_real_text() {
let (pages, toc) = sample_manual();
assert_eq!(pages.len(), toc.len(), "one TOC entry per page");
assert!(!pages.is_empty(), "manual has pages");
for (i, bytes) in pages.iter().enumerate() {
// The SVG source must actually contain glyph text (not just shapes).
let src = std::str::from_utf8(bytes).expect("utf8 svg");
assert!(src.contains("<text"), "page {i} carries <text> elements");
// It parses against the docview fontdb and rasterizes glyph ink.
let page = DocPage::from_svg_bytes(bytes).expect("sample page parses");
let img = page.rasterize(2.0).expect("page rasterizes");
let ink = dark_ink(&img);
assert!(
ink > 400,
"page {i} rendered visible glyph ink (the embedded font shaped \
the <text>); got {ink} dark pixels — boxes-only pages would never \
reach this from the text lines"
);
}
}
}