fix(annot): cap the bumps of a cloudy outline

The cloud border clamped bumps per edge only, so a polygon with many long
edges asked for 10,000 cubic segments on each one. The bump radius now
grows so the whole outline draws at most 20,000 bumps plus one per edge.
This commit is contained in:
aiongg
2026-10-22 17:08:54 +00:00
parent 986a64bc77
commit e07942c829
2 changed files with 14 additions and 0 deletions
+6
View File
@@ -120,12 +120,18 @@ fn smooth_curve(pts: &[(f64, f64)]) -> String {
smooth_segments(pts).iter().map(|[a, b, e]| format!("{} {} {} {} {} {} c\n", n(a.0), n(a.1), n(b.0), n(b.1), n(e.0), n(e.1))).collect()
}
/// Bumps one cloudy outline draws at most (plus one per edge); past it the bumps grow.
pub(crate) const MAX_CLOUD_BUMPS: usize = 20_000;
/// A closed cloudy outline through `pts` (§12.5.4 `/BE /S /C`): each edge becomes a row of
/// half-circle bumps of about `r`, bulging outwards.
pub fn cloud_path(pts: &[(f64, f64)], r: f64) -> String {
let area: f64 = pts.iter().zip(pts.iter().cycle().skip(1)).map(|(a, b)| a.0 * b.1 - b.0 * a.1).sum();
// Outward normal of an edge: to the right of travel for counter-clockwise outlines.
let sign = if area >= 0.0 { 1.0 } else { -1.0 };
let perimeter: f64 = pts.iter().zip(pts.iter().cycle().skip(1)).map(|(a, b)| (b.0 - a.0).hypot(b.1 - a.1)).sum();
// Each edge rounds to at least one bump, so the total stays within the budget plus one per edge.
let r = r.max(perimeter / (2.0 * MAX_CLOUD_BUMPS as f64));
let mut c = String::new();
for (i, (a, b)) in pts.iter().zip(pts.iter().cycle().skip(1)).enumerate() {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
+8
View File
@@ -1504,3 +1504,11 @@ fn typed_signature_uses_nonzero_winding_fill() {
assert!(ap.ends_with("h\nf\n"), "expected non-zero winding fill (`f`), got: {ap}");
assert!(!ap.contains("f*"), "must not use even-odd fill (`f*`): {ap}");
}
#[test]
fn cloud_outline_has_a_bump_budget() {
// 50 long edges would each ask for 10,000 bumps of radius 1.
let pts: Vec<(f64, f64)> = (0..50).map(|i| (f64::from(i) * std::f64::consts::TAU / 50.0).sin_cos()).map(|(s, c)| (1e7 * c, 1e7 * s)).collect();
let bumps = appearance::cloud_path(&pts, 1.0).lines().filter(|l| l.ends_with(" c")).count();
assert!((pts.len()..=appearance::MAX_CLOUD_BUMPS + 2 * pts.len()).contains(&bumps), "{bumps} bumps");
}