Keep a zoomed-in profile's hover fill: count the slabs each edge crosses (#336)

fill_triangles bounded the slab triangulator by edges x slabs, as if every
edge crossed every slab. A ring sampled finely at a close zoom has hundreds
of edges and slabs but only four edges in each slab, so the estimate ran a
hundred times over the hover highlight's budget and the fill was dropped,
leaving only the outline. Count the slabs each edge actually spans instead.

Region2::triangulate tested every edge against every slab, so its work was
the overestimate. It now sweeps upward with the edges crossing the current
slab, giving the same triangles in time proportional to them.

Fixes #164

Co-authored-by: Vitali M. <6042958+vitalibr@users.noreply.github.com>
This commit is contained in:
Vitali M.andVitali M. authored and GitHub committed 2026-10-12 13:47:11 -04:00
1 parent e6a95615a0
commit b2fc6d1fac
3 files changed
+108 -8

No files matched your search

+64 -1
View File
@@ -527,11 +527,23 @@ impl Region2 {
ys.sort_by(f64::total_cmp);
ys.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
let x_at = |e: &(Vec2, Vec2), y: f64| e.0.x + (y - e.0.y) / (e.1.y - e.0.y) * (e.1.x - e.0.x);
// Sweep upward: an edge joins the slabs once its lower end is passed and leaves after
// its upper end, so each slab sorts only the edges crossing it (a finely sampled ring
// has hundreds of slabs and edges, but four edges in each slab).
edges.sort_by(|a, b| a.0.y.total_cmp(&b.0.y));
let mut next = 0;
let mut span: Vec<&(Vec2, Vec2)> = Vec::new();
let mut out = Vec::new();
for w in ys.windows(2) {
let (y0, y1) = (w[0], w[1]);
let ym = (y0 + y1) * 0.5;
let mut span: Vec<&(Vec2, Vec2)> = edges.iter().filter(|e| e.0.y <= ym && e.1.y >= ym).collect();
while let Some(e) = edges.get(next)
&& e.0.y <= ym
{
span.push(e);
next += 1;
}
span.retain(|e| e.1.y >= ym);
span.sort_by(|a, b| x_at(a, ym).total_cmp(&x_at(b, ym)));
for pair in span.chunks(2) {
let [l, r] = pair else { continue };
@@ -630,6 +642,57 @@ mod tests {
}
}
/// The sweep visits only the edges crossing each slab; it must give the triangles that
/// testing every edge against every slab gives, for a finely sampled ring too (#164).
#[test]
fn triangulation_sweep_matches_every_edge_against_every_slab() {
fn every_edge(r: &Region2, tol: f64) -> Vec<[Vec2; 3]> {
let mut edges = Vec::new();
for l in std::iter::once(&r.outer).chain(&r.holes) {
let pl = l.polyline(tol);
for i in 0..pl.len() {
let (a, b) = (pl[i], pl[(i + 1) % pl.len()]);
if (a.y - b.y).abs() > 1e-12 {
edges.push(if a.y < b.y { (a, b) } else { (b, a) });
}
}
}
let mut ys: Vec<f64> = edges.iter().flat_map(|(a, b): &(Vec2, Vec2)| [a.y, b.y]).collect();
ys.sort_by(f64::total_cmp);
ys.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
let x_at = |e: &(Vec2, Vec2), y: f64| e.0.x + (y - e.0.y) / (e.1.y - e.0.y) * (e.1.x - e.0.x);
let mut out = Vec::new();
for w in ys.windows(2) {
let ym = (w[0] + w[1]) * 0.5;
let mut span: Vec<_> = edges.iter().filter(|e| e.0.y <= ym && e.1.y >= ym).collect();
span.sort_by(|a, b| x_at(a, ym).total_cmp(&x_at(b, ym)));
for pair in span.chunks(2) {
let [l, r] = pair else { continue };
let (a, b) = (Vec2::new(x_at(l, w[0]), w[0]), Vec2::new(x_at(r, w[0]), w[0]));
let (c, d) = (Vec2::new(x_at(r, w[1]), w[1]), Vec2::new(x_at(l, w[1]), w[1]));
out.extend([[a, b, c], [a, c, d]]);
}
}
out
}
let comb = Loop2::polygon(&[
Vec2::new(0.0, 0.0),
Vec2::new(9.0, 0.0),
Vec2::new(9.0, 6.0),
Vec2::new(7.0, 2.0),
Vec2::new(5.0, 7.0),
Vec2::new(3.0, 2.5),
Vec2::new(1.0, 6.0),
]);
let ring = Region2 { outer: Loop2::circle(Vec2::ZERO, 45.0), holes: vec![Loop2::circle(Vec2::ZERO, 40.0).reversed()] };
let regions = [Region2 { outer: comb, holes: vec![Loop2::circle(Vec2::new(4.5, 1.2), 0.6).reversed()] }, ring];
for (r, tol) in regions.iter().zip([1e-3, 1e-4]) {
let tris = r.triangulate(tol);
assert!(tris.len() > 100);
assert_eq!(tris, every_edge(r, tol));
}
}
#[test]
fn triangulated_area() {
let sq = Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(10.0, 8.0), Vec2::new(5.0, 3.0), Vec2::new(0.0, 8.0)]);