//! Filled font glyphs: bounded font-unit meshes shared across placements and zoom rebuilds. use std::collections::HashMap; use std::sync::{Arc, OnceLock, PoisonError, RwLock}; use cadcraft_fonts::{TextGlyph, ttf}; use cadcraft_geom::{Bounds2, Mat3, Vec2}; use lyon_tessellation::geometry_builder::{BuffersBuilder, VertexBuffers}; use lyon_tessellation::math::point; use lyon_tessellation::path::Path; use lyon_tessellation::{FillOptions, FillTessellator, FillVertex}; const MAX_POINTS: usize = 16_384; const MAX_ENTRIES: usize = 8192; const MAX_CACHE_BYTES: usize = 64 << 20; const MAX_SIMPLIFY_STEPS: usize = 1_000_000; #[derive(Clone, Copy, PartialEq, Eq, Hash)] enum Detail { Full, /// A power-of-two tolerance in font units, rounded toward finer detail. Reduced(i16), /// An em occupies at most two pixels in every direction: show the ink's extent. Box, } type Key = (usize, usize, u32, u16, Detail); pub(super) struct Mesh { contours: Arc<[Vec]>, pub triangles: Vec, } struct Entry { // Keep the bytes alive: registering a replacement font must never reuse this key's address. _face: ttf::Face, mesh: Arc, } #[derive(Default)] struct Cache { entries: HashMap, bytes: usize, } /// A read-write lock: display lists are built on all cores and nearly every lookup is a hit. fn cache() -> &'static RwLock { static CACHE: OnceLock> = OnceLock::new(); CACHE.get_or_init(|| RwLock::new(Cache::default())) } /// World-space placement of font units, including the block/viewport transform. pub(super) fn placement(g: &TextGlyph, xf: Mat3) -> Mat3 { xf.then_before(Mat3 { a: g.x.x / 1000.0, b: g.x.y / 1000.0, c: g.y.x / 1000.0, d: g.y.y / 1000.0, e: g.origin.x, f: g.origin.y }) } fn detail(xf: Mat3, pixel: f64) -> Detail { // The Frobenius norm bounds the largest stretch, including shear, mirroring and unequal // block scales. A font-unit error below pixel / stretch is below a pixel in every direction. let stretch = xf.a.hypot(xf.b).hypot(xf.c.hypot(xf.d)); if !(pixel.is_finite() && pixel > 0.0 && stretch.is_finite() && stretch > 0.0) { return Detail::Full; } if stretch * 1000.0 <= pixel * 2.0 { return Detail::Box; } let tolerance = pixel * 0.25 / stretch; if !tolerance.is_finite() || tolerance < 0.0625 { Detail::Full } else { Detail::Reduced(tolerance.log2().floor().clamp(-4.0, 16.0) as i16) } } pub(super) fn mesh(g: &TextGlyph, xf: Mat3, pixel: f64) -> Arc { let detail = detail(xf, pixel); let key = (g.face.bytes.as_ptr() as usize, g.face.bytes.len(), g.face.index, g.gid, detail); if let Some(entry) = cache().read().unwrap_or_else(PoisonError::into_inner).entries.get(&key) { return entry.mesh.clone(); } let contours = ttf::glyph_contours(&g.face, g.gid); let points = contours.iter().map(Vec::len).try_fold(0usize, usize::checked_add); // Font data is untrusted; don't hand unbounded or invalid paths to the tessellator. if !points.is_some_and(|n| n <= MAX_POINTS) || contours.iter().flatten().any(|p| !p.is_finite() || p.x.abs().max(p.y.abs()) > 1.0e6) { return Arc::new(Mesh { contours: Arc::default(), triangles: Vec::new() }); } let contours = match detail { Detail::Full => contours, Detail::Reduced(band) => contours.iter().map(|c| simplify(c, 2f64.powi(i32::from(band)))).collect::>().into(), Detail::Box => { let bb = Bounds2::from_points(contours.iter().flatten().copied()); if bb.is_empty() { Arc::default() } else { vec![vec![bb.min, Vec2::new(bb.max.x, bb.min.y), bb.max, Vec2::new(bb.min.x, bb.max.y), bb.min]].into() } } }; let mesh = Arc::new(Mesh { triangles: triangulate(&contours), contours }); let points = mesh.contours.iter().map(Vec::capacity).sum::().saturating_add(mesh.triangles.capacity()); let bytes = points .saturating_mul(std::mem::size_of::()) .saturating_add(mesh.contours.len().saturating_mul(std::mem::size_of::>())) .saturating_add(std::mem::size_of::() + std::mem::size_of::() + std::mem::size_of::()); if bytes <= MAX_CACHE_BYTES { let mut c = cache().write().unwrap_or_else(PoisonError::into_inner); if c.entries.len() >= MAX_ENTRIES || c.bytes.saturating_add(bytes) > MAX_CACHE_BYTES { c.entries.clear(); c.bytes = 0; } if !c.entries.contains_key(&key) { c.bytes = c.bytes.saturating_add(bytes); c.entries.insert(key, Entry { _face: g.face.clone(), mesh: mesh.clone() }); } } mesh } /// Iterative Douglas-Peucker on a closed contour. Never collapse a hole to a line or recurse /// on font data; preserve the original if the work budget is exhausted. fn simplify(contour: &[Vec2], tolerance: f64) -> Vec { if contour.len() < 5 { return contour.to_vec(); } let mut points = contour.to_vec(); if let (Some(&first), Some(&last)) = (points.first(), points.last()) && first != last { points.push(first); } let last = points.len().saturating_sub(1); let mut keep = vec![false; points.len()]; if let Some(k) = keep.first_mut() { *k = true; } if let Some(k) = keep.last_mut() { *k = true; } let mut pending = vec![(0usize, last)]; let mut steps = 0usize; while let Some((start, end)) = pending.pop() { let (Some(&a), Some(&b)) = (points.get(start), points.get(end)) else { continue }; let ab = b - a; let len2 = ab.len2(); let mut furthest = None; let mut distance = tolerance; for (i, &p) in points.iter().enumerate().take(end).skip(start + 1) { steps += 1; if steps > MAX_SIMPLIFY_STEPS { return contour.to_vec(); } let t = if len2 > 0.0 { ((p - a).dot(ab) / len2).clamp(0.0, 1.0) } else { 0.0 }; let d = p.dist(a + ab * t); if d > distance { distance = d; furthest = Some(i); } } if let Some(i) = furthest { if let Some(k) = keep.get_mut(i) { *k = true; } pending.extend([(start, i), (i, end)]); } } let result: Vec<_> = points.into_iter().zip(keep).filter_map(|(p, keep)| keep.then_some(p)).collect(); if result.len() >= 4 { result } else { contour.to_vec() } } /// Monotone tessellation with even-odd holes and intersections. Work in font units so f32 /// precision is independent of drawing coordinates; the placement stays f64. The existing /// slab filler remains the fallback when tessellation fails (including its u16 vertex limit). fn triangulate(contours: &[Vec]) -> Vec { let mut path = Path::builder(); for contour in contours { let Some(&first) = contour.first().filter(|_| contour.len() >= 3) else { continue }; path.begin(point(first.x as f32, first.y as f32)); for p in contour.iter().skip(1) { path.line_to(point(p.x as f32, p.y as f32)); } path.end(true); } let mut buffers: VertexBuffers = VertexBuffers::new(); let result = FillTessellator::new().tessellate_path( &path.build(), &FillOptions::even_odd(), &mut BuffersBuilder::new(&mut buffers, |v: FillVertex<'_>| { let p = v.position(); Vec2::new(f64::from(p.x), f64::from(p.y)) }), ); if result.is_err() { return crate::fill::triangulate_evenodd(contours); } buffers.indices.into_iter().filter_map(|i| buffers.vertices.get(usize::from(i)).copied()).collect() }