mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-27 04:16:48 -06:00
Squash of PR #654 by @aldozar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
7.7 KiB
Rust
193 lines
7.7 KiB
Rust
//! 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<Vec2>]>,
|
|
pub triangles: Vec<Vec2>,
|
|
}
|
|
|
|
struct Entry {
|
|
// Keep the bytes alive: registering a replacement font must never reuse this key's address.
|
|
_face: ttf::Face,
|
|
mesh: Arc<Mesh>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Cache {
|
|
entries: HashMap<Key, Entry>,
|
|
bytes: usize,
|
|
}
|
|
|
|
/// A read-write lock: display lists are built on all cores and nearly every lookup is a hit.
|
|
fn cache() -> &'static RwLock<Cache> {
|
|
static CACHE: OnceLock<RwLock<Cache>> = 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<Mesh> {
|
|
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::<Vec<_>>().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::<usize>().saturating_add(mesh.triangles.capacity());
|
|
let bytes = points
|
|
.saturating_mul(std::mem::size_of::<Vec2>())
|
|
.saturating_add(mesh.contours.len().saturating_mul(std::mem::size_of::<Vec<Vec2>>()))
|
|
.saturating_add(std::mem::size_of::<Entry>() + std::mem::size_of::<Key>() + std::mem::size_of::<Mesh>());
|
|
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<Vec2> {
|
|
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<Vec2>]) -> Vec<Vec2> {
|
|
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<Vec2, u16> = 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()
|
|
}
|