Fix filled text zoom freezes with cached glyph meshes (#651)
Squash of PR #651 by @PedroGuille. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
90cd443000
commit
2404719c3b
|
|
@ -692,6 +692,7 @@ dependencies = [
|
||||||
"cadcraft-doc",
|
"cadcraft-doc",
|
||||||
"cadcraft-fonts",
|
"cadcraft-fonts",
|
||||||
"cadcraft-geom",
|
"cadcraft-geom",
|
||||||
|
"lyon_tessellation",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
]
|
]
|
||||||
|
|
@ -1399,6 +1400,12 @@ dependencies = [
|
||||||
"zlib-rs",
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float_next_after"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -2143,6 +2150,38 @@ version = "0.4.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lyon_geom"
|
||||||
|
version = "1.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"euclid",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lyon_path"
|
||||||
|
version = "1.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e"
|
||||||
|
dependencies = [
|
||||||
|
"lyon_geom",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lyon_tessellation"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005"
|
||||||
|
dependencies = [
|
||||||
|
"float_next_after",
|
||||||
|
"lyon_path",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ rayon = "1"
|
||||||
|
|
||||||
# rendering / formats
|
# rendering / formats
|
||||||
tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd", "png-format"] }
|
tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd", "png-format"] }
|
||||||
|
lyon_tessellation = "1.0.22"
|
||||||
skrifa = "0.47"
|
skrifa = "0.47"
|
||||||
# Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; the version egui's epaint uses)
|
# Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; the version egui's epaint uses)
|
||||||
# and the Unicode Bidirectional Algorithm for right-to-left drawing text.
|
# and the Unicode Bidirectional Algorithm for right-to-left drawing text.
|
||||||
|
|
|
||||||
|
|
@ -580,7 +580,7 @@ pub fn fallback_for(c: char) -> Option<Face> {
|
||||||
/// font's cap height.
|
/// font's cap height.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct GlyphOutline {
|
pub(crate) struct GlyphOutline {
|
||||||
pub(crate) contours: Vec<Vec<Vec2>>,
|
pub(crate) contours: Arc<[Vec<Vec2>]>,
|
||||||
pub(crate) advance: f64,
|
pub(crate) advance: f64,
|
||||||
pub(crate) cap: f64,
|
pub(crate) cap: f64,
|
||||||
/// The font maps the character (false: the missing-glyph shape).
|
/// The font maps the character (false: the missing-glyph shape).
|
||||||
|
|
@ -691,6 +691,12 @@ pub(crate) fn glyph_by_id(face: &Face, gid: u32) -> Arc<GlyphOutline> {
|
||||||
outline(face, GlyphKey::Id(gid))
|
outline(face, GlyphKey::Id(gid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Closed, flattened contours of a glyph at 1000 font units per em. Shared with the outline
|
||||||
|
/// cache so renderers can tessellate once before applying each occurrence's affine placement.
|
||||||
|
pub fn glyph_contours(face: &Face, gid: u16) -> Arc<[Vec<Vec2>]> {
|
||||||
|
glyph_by_id(face, u32::from(gid)).contours.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn outline(face: &Face, k: GlyphKey) -> Arc<GlyphOutline> {
|
fn outline(face: &Face, k: GlyphKey) -> Arc<GlyphOutline> {
|
||||||
let key = (face.bytes.as_ptr() as usize, face.bytes.len(), face.index, k);
|
let key = (face.bytes.as_ptr() as usize, face.bytes.len(), face.index, k);
|
||||||
if let Some(g) = glyph_cache().lock().unwrap_or_else(PoisonError::into_inner).get(&key) {
|
if let Some(g) = glyph_cache().lock().unwrap_or_else(PoisonError::into_inner).get(&key) {
|
||||||
|
|
@ -709,7 +715,7 @@ fn outline(face: &Face, k: GlyphKey) -> Arc<GlyphOutline> {
|
||||||
}
|
}
|
||||||
let advance = f64::from(f.glyph_metrics(Size::new(UPEM), LocationRef::default()).advance_width(gid).unwrap_or(UPEM * 0.5));
|
let advance = f64::from(f.glyph_metrics(Size::new(UPEM), LocationRef::default()).advance_width(gid).unwrap_or(UPEM * 0.5));
|
||||||
let (cap, mapped, gid) = (cap_height(&f), mapped.is_some(), gid.to_u32());
|
let (cap, mapped, gid) = (cap_height(&f), mapped.is_some(), gid.to_u32());
|
||||||
let g = Arc::new(GlyphOutline { contours: pen.contours, advance, cap, mapped, gid, face: Some(face.clone()) });
|
let g = Arc::new(GlyphOutline { contours: pen.contours.into(), advance, cap, mapped, gid, face: Some(face.clone()) });
|
||||||
let mut cache = glyph_cache().lock().unwrap_or_else(PoisonError::into_inner);
|
let mut cache = glyph_cache().lock().unwrap_or_else(PoisonError::into_inner);
|
||||||
if cache.len() > MAX_CACHED_GLYPHS {
|
if cache.len() > MAX_CACHED_GLYPHS {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ cadcraft-color = { workspace = true }
|
||||||
cadcraft-doc = { workspace = true }
|
cadcraft-doc = { workspace = true }
|
||||||
cadcraft-fonts = { workspace = true }
|
cadcraft-fonts = { workspace = true }
|
||||||
tiny-skia = { workspace = true }
|
tiny-skia = { workspace = true }
|
||||||
|
lyon_tessellation = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ pub mod pen;
|
||||||
mod plotstyle;
|
mod plotstyle;
|
||||||
mod point;
|
mod point;
|
||||||
pub mod raster;
|
pub mod raster;
|
||||||
|
mod text;
|
||||||
mod tolerance;
|
mod tolerance;
|
||||||
pub mod units;
|
pub mod units;
|
||||||
mod wide;
|
mod wide;
|
||||||
|
|
@ -172,7 +173,8 @@ impl DisplayList {
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
/// Chord tolerance in world units (about half a pixel at the current zoom).
|
/// Chord tolerance in world units (about half a pixel at the current zoom).
|
||||||
pub tolerance: f64,
|
pub tolerance: f64,
|
||||||
/// Dashes shorter than this (world units) are drawn as continuous lines.
|
/// Two pixels in world units: shorter dashes are continuous and filled font text uses
|
||||||
|
/// screen-size detail. Zero keeps full detail (plots and image/vector exports).
|
||||||
pub min_dash: f64,
|
pub min_dash: f64,
|
||||||
/// Draw text (off for very coarse previews).
|
/// Draw text (off for very coarse previews).
|
||||||
pub text: bool,
|
pub text: bool,
|
||||||
|
|
@ -289,6 +291,9 @@ impl Builder<'_> {
|
||||||
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw, kind: Kind::Polyline, start, len: pts.len() as u32 });
|
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw, kind: Kind::Polyline, start, len: pts.len() as u32 });
|
||||||
}
|
}
|
||||||
fn tris(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2]) {
|
fn tris(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2]) {
|
||||||
|
self.tris_at(ctx, color, tris, ctx.xf);
|
||||||
|
}
|
||||||
|
fn tris_at(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2], xf: Mat3) {
|
||||||
if tris.len() < 3 {
|
if tris.len() < 3 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +301,7 @@ impl Builder<'_> {
|
||||||
let (color, _) = self.styled(color, 0.0);
|
let (color, _) = self.styled(color, 0.0);
|
||||||
let start = self.list.tris.len() as u32;
|
let start = self.list.tris.len() as u32;
|
||||||
for p in tris {
|
for p in tris {
|
||||||
let q = ctx.xf.apply(*p);
|
let q = xf.apply(*p);
|
||||||
self.list.bounds.add(q);
|
self.list.bounds.add(q);
|
||||||
self.list.tris.push(q);
|
self.list.tris.push(q);
|
||||||
}
|
}
|
||||||
|
|
@ -312,16 +317,26 @@ impl Builder<'_> {
|
||||||
}
|
}
|
||||||
let fill = self.opts.fill && ctx.d.header.i64("TEXTFILL", 1) != 0;
|
let fill = self.opts.fill && ctx.d.header.i64("TEXTFILL", 1) != 0;
|
||||||
// Plots note which filled outlines are font glyphs (see `DisplayList::texts`).
|
// Plots note which filled outlines are font glyphs (see `DisplayList::texts`).
|
||||||
let mut text = if self.plotting && fill { sh.text.iter().peekable() } else { [].iter().peekable() };
|
let mut text = sh.text.iter().peekable();
|
||||||
|
let pixel = if self.plotting { 0.0 } else { self.opts.min_dash * 0.5 };
|
||||||
for (i, g) in sh.glyphs.iter().enumerate() {
|
for (i, g) in sh.glyphs.iter().enumerate() {
|
||||||
if fill {
|
if fill {
|
||||||
let tris = fill::triangulate_evenodd(g);
|
|
||||||
let at = self.list.prims.len();
|
|
||||||
self.tris(ctx, color, &tris);
|
|
||||||
while text.next_if(|t| t.outline < i).is_some() {}
|
while text.next_if(|t| t.outline < i).is_some() {}
|
||||||
if let Some(t) = text.next_if(|t| t.outline == i)
|
let glyph = text.next_if(|t| t.outline == i);
|
||||||
|
let at = self.list.prims.len();
|
||||||
|
let len = if let Some(t) = glyph {
|
||||||
|
let xf = crate::text::placement(t, ctx.xf);
|
||||||
|
let mesh = crate::text::mesh(t, xf, pixel);
|
||||||
|
self.tris_at(ctx, color, &mesh.triangles, xf);
|
||||||
|
mesh.triangles.len()
|
||||||
|
} else {
|
||||||
|
let tris = fill::triangulate_evenodd(g);
|
||||||
|
self.tris(ctx, color, &tris);
|
||||||
|
tris.len()
|
||||||
|
};
|
||||||
|
if let Some(t) = glyph.filter(|_| self.plotting)
|
||||||
&& self.list.prims.len() > at
|
&& self.list.prims.len() > at
|
||||||
&& let (Ok(prim), Ok(verts)) = (u32::try_from(at), u32::try_from(tris.len()))
|
&& let (Ok(prim), Ok(verts)) = (u32::try_from(at), u32::try_from(len))
|
||||||
{
|
{
|
||||||
let xf = &ctx.xf;
|
let xf = &ctx.xf;
|
||||||
let glyph = cadcraft_fonts::TextGlyph { origin: xf.apply(t.origin), x: xf.apply_vec(t.x), y: xf.apply_vec(t.y), ..t.clone() };
|
let glyph = cadcraft_fonts::TextGlyph { origin: xf.apply(t.origin), x: xf.apply_vec(t.x), y: xf.apply_vec(t.y), ..t.clone() };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
//! Filled font glyphs: bounded font-unit meshes shared across placements and zoom rebuilds.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache() -> &'static Mutex<Cache> {
|
||||||
|
static CACHE: OnceLock<Mutex<Cache>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| Mutex::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().lock().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().lock().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()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue