Files
figcraft/crates/pathops/src/budget_tests.rs
T
Dylan M. Taylor 554ad0ea77 pathops: budget the closed faces input, share the dash allowance, index point incidence (#2249)
* pathops: check closed inputs before faces normalises, share the dash allowance across subpaths, index point incidence once

* pathops: leave out move-only subpaths when closing, budget dashes by the fitted pattern, index only join points
2026-11-11 14:49:49 -05:00

128 lines
5.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Input caps for the sweep: huge inputs are refused at once with a clear error, real ones run.
use std::time::{Duration, Instant};
use figcraft_doc::BoolOp;
use figcraft_geom::kurbo::{BezPath, Rect, Shape};
use crate::{Complexity, FoldBudget, MAX_CROSSINGS, MAX_FOLD_SEGMENTS, MAX_SEGMENTS, PathOpsError, boolean, complexity, faces, simplify};
fn rects(rs: impl IntoIterator<Item = Rect>) -> BezPath {
let mut p = BezPath::new();
for r in rs {
p.extend(r.path_elements(0.1));
}
p
}
/// `n` long thin bars across and `n` down: each bar across crosses each bar down 4 times.
fn grid(n: usize) -> (BezPath, BezPath) {
let len = n as f64 * 4.0;
let across = rects((0..n).map(|i| Rect::new(0.0, i as f64 * 4.0, len, i as f64 * 4.0 + 2.0)));
let down = rects((0..n).map(|i| Rect::new(i as f64 * 4.0, 0.0, i as f64 * 4.0 + 2.0, len)));
(across, down)
}
/// A zigzag of `n` line segments.
fn zigzag(n: usize) -> BezPath {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
for i in 1..=n {
p.line_to(((i % 2) as f64, i as f64 * 1e-3));
}
p
}
#[test]
fn crossings_count_the_segments_whose_bounds_overlap() {
let square = rects([Rect::new(0.0, 0.0, 1.0, 1.0)]);
assert_eq!(complexity([&square]), Some(Complexity { segments: 4, crossings: 4 }), "each side touches its two neighbours");
let apart = rects([Rect::new(0.0, 0.0, 1.0, 1.0), Rect::new(5.0, 5.0, 6.0, 6.0)]);
assert_eq!(complexity([&apart]), Some(Complexity { segments: 8, crossings: 8 }));
let mut plus = BezPath::new();
plus.move_to((0.0, 5.0));
plus.line_to((10.0, 5.0));
plus.move_to((5.0, 0.0));
plus.line_to((5.0, 10.0));
assert_eq!(complexity([&plus]).map(|c| c.crossings), Some(1));
assert_eq!(complexity([]), Some(Complexity::default()));
// Every crossing is counted, and little else: the bars touch only at their ends.
let (across, down) = grid(10);
let c = complexity([&across, &down]).unwrap();
assert_eq!(c.segments, 80);
assert!((400..=400 + 12 * 10).contains(&c.crossings), "{c:?}");
}
#[test]
fn a_pattern_of_ten_thousand_dots_is_far_within_the_caps() {
let dots = rects((0..10_000).map(|i| {
let (x, y) = (f64::from(i % 100) * 3.0, f64::from(i / 100) * 3.0);
Rect::new(x, y, x + 2.0, y + 2.0)
}));
let c = complexity([&dots]).unwrap();
assert_eq!(c, Complexity { segments: 40_000, crossings: 40_000 });
assert!(c.segments * 5 < MAX_SEGMENTS && c.crossings * 40 < MAX_CROSSINGS);
}
#[test]
fn a_boolean_past_the_crossing_cap_is_refused_before_it_runs() {
// 750 × 750 bars: 2.25 million crossings.
let (across, down) = grid(750);
let start = Instant::now();
assert_eq!(boolean(&[across.clone(), down.clone()], BoolOp::Union), Err(PathOpsError::TooComplex));
assert!(matches!(faces(&[(across, false), (down, false)]), Err(PathOpsError::TooComplex)));
assert!(start.elapsed() < Duration::from_secs(5), "{:?}", start.elapsed());
assert!(PathOpsError::TooComplex.to_string().contains("too complex to combine"));
}
#[test]
fn a_path_past_the_segment_cap_is_refused_without_reading_it_all() {
assert_eq!(complexity([&zigzag(MAX_SEGMENTS)]).map(|c| c.segments), Some(MAX_SEGMENTS), "at the cap: measured");
let huge = zigzag(MAX_SEGMENTS + 1);
assert_eq!(complexity([&huge]), None);
let start = Instant::now();
assert_eq!(simplify(&huge), Err(PathOpsError::TooComplex));
assert!(start.elapsed() < Duration::from_secs(5), "{:?}", start.elapsed());
}
#[test]
fn a_fold_stops_once_it_has_swept_its_budget() {
let (acc, p) = (zigzag(100_000), rects([Rect::new(0.0, 0.0, 1.0, 1.0)]));
let mut budget = FoldBudget::default();
let steps = (0..100).take_while(|_| budget.spend(&acc, &p).is_ok()).count();
assert_eq!(steps, MAX_FOLD_SEGMENTS / 100_004);
assert_eq!(budget.spend(&BezPath::new(), &BezPath::new()), Err(PathOpsError::TooComplex), "spent stays spent");
}
#[test]
fn overlapping_rectangle_bounds_count_self_and_cross_pairs() {
let shapes = rects([Rect::new(0.0, 0.0, 2.0, 2.0), Rect::new(1.0, 1.0, 3.0, 3.0), Rect::new(4.0, 4.0, 5.0, 5.0)]);
assert_eq!(complexity([&shapes]), Some(Complexity { segments: 12, crossings: 14 }));
}
/// Open L shapes whose segments never overlap but whose implicit closing edges all do.
#[test]
fn faces_checks_the_closed_inputs_before_normalising() {
let mut p = BezPath::new();
for i in 0..2200 {
let (x, y) = (-(i as f64), 10_000.0 + i as f64);
p.move_to((x, 0.0));
p.line_to((x, y));
p.line_to((10_000.0, y));
}
assert!(complexity([&p]).is_some_and(|c| c.crossings <= MAX_CROSSINGS), "open input passes");
assert!(matches!(faces(&[(p, false)]), Err(PathOpsError::TooComplex)));
}
#[test]
fn closing_drops_move_only_subpaths() {
let mut p = BezPath::new();
for i in 0..1000 {
p.move_to((i as f64, 0.0));
}
assert!(crate::closed(&p).elements().is_empty());
p.line_to((5.0, 5.0));
let c = crate::closed(&p);
assert_eq!(c.elements().len(), 3, "the last move, the line and the close: {:?}", c.elements());
}