mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-11 22:21:58 +00:00
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
This commit is contained in:
@@ -99,3 +99,29 @@ 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());
|
||||
}
|
||||
|
||||
@@ -57,20 +57,27 @@ impl std::fmt::Display for PathOpsError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Close every open subpath (a filled open path paints as if closed).
|
||||
/// Close every open subpath (a filled open path paints as if closed). A subpath of a lone move
|
||||
/// fills nothing and is left out.
|
||||
pub fn closed(path: &BezPath) -> BezPath {
|
||||
let mut out = BezPath::new();
|
||||
let mut open = false;
|
||||
let mut pending: Option<PathEl> = None;
|
||||
for el in path.elements() {
|
||||
match el {
|
||||
PathEl::MoveTo(_) => {
|
||||
if open {
|
||||
out.close_path();
|
||||
}
|
||||
open = true;
|
||||
open = false;
|
||||
pending = Some(*el);
|
||||
continue;
|
||||
}
|
||||
PathEl::ClosePath => open = false,
|
||||
_ => {}
|
||||
_ => open = true,
|
||||
}
|
||||
if let Some(m) = pending.take() {
|
||||
out.push(m);
|
||||
}
|
||||
out.push(*el);
|
||||
}
|
||||
@@ -240,7 +247,10 @@ pub fn faces(paths: &[(BezPath, bool)]) -> Result<Vec<ArrangementFace>, PathOpsE
|
||||
// corner folds over itself and touches the shape's fill) leave the winding on each side of a
|
||||
// segment ambiguous where they touch, which made the sweep report overlapping faces. Reducing
|
||||
// every input to the simple contours of the area it fills first keeps the faces disjoint.
|
||||
let closed_paths: Vec<BezPath> = paths.iter().map(|(p, even_odd)| plain(&closed(p), *even_odd)).collect();
|
||||
// Closing adds edges, so what `plain` sweeps is checked first.
|
||||
let closed_inputs: Vec<BezPath> = paths.iter().map(|(p, _)| closed(p)).collect();
|
||||
budget::check(&closed_inputs)?;
|
||||
let closed_paths: Vec<BezPath> = closed_inputs.iter().zip(paths).map(|(p, (_, even_odd))| plain(p, *even_odd)).collect();
|
||||
if closed_paths.iter().all(|p| p.elements().is_empty()) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
@@ -66,10 +66,23 @@ impl Styled {
|
||||
}
|
||||
// Each point's own join: a short corner along its two segments stroked with that join.
|
||||
let reach = (width * 0.05).max(0.2);
|
||||
// Segments per point, built once (a scan of every segment per point is quadratic); like `incident`, a loop segment counts at its `a` end only.
|
||||
// Only points that draw a join of their own get a list.
|
||||
let mut incident: std::collections::HashMap<usize, Vec<(usize, bool)>> =
|
||||
(0..self.net.vertices.len()).filter(|v| self.breaks(*v)).map(|v| (v, Vec::new())).collect();
|
||||
for (i, s) in self.net.segments.iter().enumerate() {
|
||||
if let Some(list) = incident.get_mut(&s.a) {
|
||||
list.push((i, true));
|
||||
}
|
||||
if s.b != s.a
|
||||
&& let Some(list) = incident.get_mut(&s.b)
|
||||
{
|
||||
list.push((i, false));
|
||||
}
|
||||
}
|
||||
for (v, x) in self.net.vertices.iter().enumerate() {
|
||||
let Some(join) = x.join.filter(|_| self.breaks(v)) else { continue };
|
||||
let inc = self.net.incident(v);
|
||||
let [(s1, a1), (s2, a2)] = inc.as_slice() else { continue };
|
||||
let Some([(s1, a1), (s2, a2)]) = incident.get(&v).map(Vec::as_slice) else { continue };
|
||||
let (Some(d1), Some(d2)) = (self.net.tangent_out(*s1, *a1), self.net.tangent_out(*s2, *a2)) else { continue };
|
||||
let p = x.point();
|
||||
let mut corner = BezPath::new();
|
||||
|
||||
@@ -262,7 +262,12 @@ const DASH_ACCURACY: f64 = 0.05;
|
||||
/// Whether `pattern` (from [`dash_pattern`]) makes at most [`MAX_DASHES`] dashes along `len`;
|
||||
/// past that a stroke draws solid instead of one outline per dash (#1060).
|
||||
pub(crate) fn dashes_fit(len: f64, pattern: &[f64]) -> bool {
|
||||
len / pattern.iter().sum::<f64>() * (pattern.len() as f64) < MAX_DASHES as f64
|
||||
dash_count(len, pattern) < MAX_DASHES as f64
|
||||
}
|
||||
|
||||
/// About how many dashes `pattern` makes along `len`.
|
||||
fn dash_count(len: f64, pattern: &[f64]) -> f64 {
|
||||
len / pattern.iter().sum::<f64>() * (pattern.len() as f64)
|
||||
}
|
||||
|
||||
/// Resample a polyline so no step is longer than `max_step`; returns the points and the length at
|
||||
@@ -303,6 +308,8 @@ pub fn variable_outline(path: &BezPath, width: f64, props: &StrokeProps, profile
|
||||
let style = Style { join: props.join, miter_limit: props.miter_limit.clamp(1.0, 100.0), cap_start, cap_end };
|
||||
let pattern = dash_pattern(props);
|
||||
let mut budget = MAX_WIDTH_SAMPLES;
|
||||
// The dashes of all subpaths together stay within MAX_DASHES; the rest draw solid.
|
||||
let mut dash_budget = MAX_DASHES as f64;
|
||||
let mut out = BezPath::new();
|
||||
for line in &lines {
|
||||
let len = line.length();
|
||||
@@ -317,9 +324,12 @@ pub fn variable_outline(path: &BezPath, width: f64, props: &StrokeProps, profile
|
||||
[0.5 * width * right, 0.5 * width * left]
|
||||
})
|
||||
.collect();
|
||||
match &pattern {
|
||||
Some(pat) if dashes_fit(len, pat) => {
|
||||
let (pat, offset) = line_dashes(len, line.closed, pat);
|
||||
// Budgeted by the pattern as fitted to this line (a short line still takes a whole pattern).
|
||||
let fit = pattern.as_ref().map(|pat| line_dashes(len, line.closed, pat));
|
||||
let used = fit.as_ref().map_or(0.0, |(pat, _)| dash_count(len, pat).max(pat.len() as f64));
|
||||
match fit {
|
||||
Some((pat, offset)) if used < dash_budget && dashes_fit(len, &pat) => {
|
||||
dash_budget -= used;
|
||||
for (p, h) in dashes(&pts, &hw, &pat, offset) {
|
||||
outline_piece(&mut out, &p, &h, false, style);
|
||||
}
|
||||
|
||||
@@ -145,3 +145,31 @@ fn hostile_and_huge_input_is_bounded() {
|
||||
let s = stroke_outline(&line(1e6), &tiny, false);
|
||||
assert!(s.elements().len() < 1_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashes_of_all_subpaths_share_one_allowance() {
|
||||
let tiny = StrokeProps { dashes: vec![1.0, 1.0], ..props(1.0, Some(WidthProfile::uniform())) };
|
||||
let many = |n: usize| {
|
||||
let mut p = BezPath::new();
|
||||
for i in 0..n {
|
||||
p.move_to((0.0, i as f64 * 4.0));
|
||||
p.line_to((15_000.0, i as f64 * 4.0));
|
||||
}
|
||||
variable_outline(&p, 1.0, &tiny, &WidthProfile::uniform()).unwrap().elements().len()
|
||||
};
|
||||
// The later subpaths draw solid once the first has used most of the allowance.
|
||||
assert!(many(10) < 3 * many(1), "{} vs {}", many(10), many(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_short_subpath_is_budgeted_by_the_whole_pattern_it_takes() {
|
||||
// 100 one-unit lines, each fitted to a 4,000-entry pattern: ~400,000 dashes if not budgeted.
|
||||
let long = StrokeProps { dashes: vec![1.0; 4000], ..props(1.0, Some(WidthProfile::uniform())) };
|
||||
let mut p = BezPath::new();
|
||||
for i in 0..100 {
|
||||
p.move_to((0.0, i as f64 * 4.0));
|
||||
p.line_to((1.0, i as f64 * 4.0));
|
||||
}
|
||||
let n = variable_outline(&p, 1.0, &long, &WidthProfile::uniform()).unwrap().elements().len();
|
||||
assert!(n < 20_000 * 12, "{n}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user