From aaa5172ba2fb6bc8fa0efe72004d8d21c870e1b0 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 20:40:30 +0000 Subject: [PATCH] fix: bound region grid registrations, island containment tests, snap_ends visits, inference duplicate searches and font name decoding --- crates/constraints/src/auto.rs | 18 ++++++++++++------ crates/fonts/src/ttf.rs | 10 +++++++++- crates/geom/src/region.rs | 27 +++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/crates/constraints/src/auto.rs b/crates/constraints/src/auto.rs index 211be6b93..f1b5def7c 100644 --- a/crates/constraints/src/auto.rs +++ b/crates/constraints/src/auto.rs @@ -232,6 +232,10 @@ pub fn infer_placed(d: &Drawing, placed: &[Handle], others: &[Handle], o: &Infer let mut extra = Vec::new(); let mut on = Vec::new(); for (rp, p) in g.points.iter().filter(|(r, _)| placed.contains(&r.handle)) { + // Nothing is added once the candidates are full, so the points left need no search. + if extra.len() + on.len() + out.len() >= MAX_CANDIDATES { + break; + } if out.iter().any(|c| c.kind == ConstraintKind::Coincident && c.refs.contains(rp)) { continue; } @@ -267,20 +271,22 @@ pub fn infer_placed(d: &Drawing, placed: &[Handle], others: &[Handle], o: &Infer // Perpendicular lines meeting in a T (a perpendicular snap onto the middle of a line). let atol = if o.angle_tolerance.is_finite() { o.angle_tolerance.clamp(0.0, 45.0).to_radians() } else { 0.0 }; let allow_per = o.types.is_empty() || o.types.iter().any(|t| t.eq_ignore_ascii_case(ConstraintKind::Perpendicular.name())); - for (r1, a1, b1) in g.lines.iter().filter(|(r, _, _)| placed.contains(&r.handle)) { + 'per: for (r1, a1, b1) in g.lines.iter().filter(|(r, _, _)| allow_per && placed.contains(&r.handle)) { for (r2, a2, b2) in g.lines.iter().filter(|(r, _, _)| r.handle != r1.handle) { + // Full: nothing more can be added, so the pairs left need no geometry or duplicate search. + if out.len() >= MAX_CANDIDATES { + break 'per; + } let (l1, l2) = (Line::new(*a1, *b1), Line::new(*a2, *b2)); let touches = |l: &Line, p: Vec2| l.dist(p) <= tol && (0.0..=1.0).contains(&l.param_of(p)); let tee = [*a1, *b1].iter().any(|p| touches(&l2, *p)) || [*a2, *b2].iter().any(|p| touches(&l1, *p)); let ang = ang_between(*b1 - *a1, *b2 - *a2); - let dup = out.iter().any(|c| c.kind == ConstraintKind::Perpendicular && c.refs.contains(r1) && c.refs.contains(r2)); - if allow_per - && tee - && !dup + // The duplicate search scans every candidate, so it comes after the cheap tests. + if tee && (std::f64::consts::FRAC_PI_2 - ang).abs() <= atol && l1.len() > tol && l2.len() > tol - && out.len() < MAX_CANDIDATES + && !out.iter().any(|c| c.kind == ConstraintKind::Perpendicular && c.refs.contains(r1) && c.refs.contains(r2)) { out.push(Constraint { id: 0, kind: ConstraintKind::Perpendicular, refs: vec![*r1, *r2], name: String::new(), expr: String::new() }); } diff --git a/crates/fonts/src/ttf.rs b/crates/fonts/src/ttf.rs index bcf610e0a..ebcf92447 100644 --- a/crates/fonts/src/ttf.rs +++ b/crates/fonts/src/ttf.rs @@ -89,7 +89,11 @@ impl Face { let Some(f) = self.font() else { return Vec::new() }; [skrifa::string::StringId::FAMILY_NAME, skrifa::string::StringId::TYPOGRAPHIC_FAMILY_NAME, skrifa::string::StringId::FULL_NAME] .into_iter() - .flat_map(|id| f.localized_strings(id).map(|s| norm(&s.to_string())).collect::>()) + // A name table can hold many records that point at one big string: at most + // MAX_NAME_RECORDS per name id, each decoded only up to MAX_NAME_CHARS characters. + .flat_map(|id| { + f.localized_strings(id).take(MAX_NAME_RECORDS).map(|s| norm(&s.chars().take(MAX_NAME_CHARS).collect::())).collect::>() + }) .collect() } } @@ -104,6 +108,10 @@ pub fn faces(bytes: &Arc>) -> Vec { } const MAX_FACES_PER_FILE: u32 = 32; +/// Name records read per name id of a face, and characters read of each: real fonts have a few +/// dozen short ones. +const MAX_NAME_RECORDS: usize = 64; +const MAX_NAME_CHARS: usize = 256; /// Bounds of the missing-glyph search: files loaded, bytes loaded, faces checked per character. const MAX_FALLBACK_FILES: usize = 24; const MAX_FALLBACK_BYTES: usize = 160 << 20; diff --git a/crates/geom/src/region.rs b/crates/geom/src/region.rs index c186534c9..085556525 100644 --- a/crates/geom/src/region.rs +++ b/crates/geom/src/region.rs @@ -110,6 +110,10 @@ fn build(polys: &[Vec], tol: f64) -> Option { }; let mut grid: HashMap<(usize, usize), Vec> = HashMap::new(); let mut long: Vec = Vec::new(); + // Every cell a segment is registered in is an index kept: past this many in all (each segment + // may take up to 4096 cells), the input is refused as it is for too much pair work. + const MAX_REGISTRATIONS: usize = 20_000_000; + let mut registered = 0usize; for (i, (a, b)) in segs.iter().enumerate() { let (x0, y0) = cell(a.min(*b)); let (x1, y1) = cell(a.max(*b)); @@ -118,6 +122,10 @@ fn build(polys: &[Vec], tol: f64) -> Option { long.push(i); continue; } + registered = registered.saturating_add((x1 - x0 + 1) * (y1 - y0 + 1)); + if registered > MAX_REGISTRATIONS { + return None; + } for gx in x0..=x1 { for gy in y0..=y1 { grid.entry((gx, gy)).or_default().push(i); @@ -424,19 +432,32 @@ fn islands(g: &Graph, outer: &[usize], p: Vec2) -> Vec { let mut starts: Vec<(usize, usize)> = start.into_iter().collect(); starts.sort_unstable(); let mut out = Vec::new(); + // Each containment test scans a whole polygon, accepted or not: they share one allowance of + // polygon edges, and the search ends when it is spent. + let mut edges_left: usize = 100_000_000; for (c, u) in starts { if out.len() >= MAX_ISLANDS { break; } let Some(&pu) = core.pts.get(u) else { continue }; - if !ob.contains(pu) || !point_in_polygon(&outer_pts, pu) { + if !ob.contains(pu) { + continue; + } + let Some(left) = edges_left.checked_sub(outer_pts.len()) else { break }; + edges_left = left; + if !point_in_polygon(&outer_pts, pu) { continue; } let steps = edges.get(&c).copied().unwrap_or(0).saturating_add(4); let Some(lp) = outside(&core, u, steps) else { continue }; let area = shoelace(&lp); let ib = Bounds2::from_points(lp.iter().copied()); - if lp.len() < 3 || !area.is_finite() || area <= 0.0 || !ob.contains_box(&ib) || point_in_polygon(&lp, p) { + if lp.len() < 3 || !area.is_finite() || area <= 0.0 || !ob.contains_box(&ib) { + continue; + } + let Some(left) = edges_left.checked_sub(lp.len()) else { break }; + edges_left = left; + if point_in_polygon(&lp, p) { continue; } let mut src = sources.remove(&c).unwrap_or_default(); @@ -457,6 +478,8 @@ fn nearest_curve(polys: &[Vec], i: usize, e: Vec2, snap: f64, tol: f64, bu if j == i { continue; } + // A curve with no segment costs a visit too, so many of them can't multiply the searches. + *budget = budget.checked_sub(1).ok_or(())?; for (k, w) in other.windows(2).enumerate() { if *budget == 0 { return Err(());