mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 19:21:22 +00:00
effects: count paint strings in 3D copies, charge dash restarts, cap trap pairs
This commit is contained in:
@@ -17,17 +17,29 @@ pub const REVOLVE: &str = "threeD.revolve";
|
||||
/// thousands of faces took gigabytes; past this the source path is drawn.
|
||||
const MAX_PAINT_COPIES: usize = 256 << 20;
|
||||
|
||||
/// About the bytes one copy of `paint` takes.
|
||||
fn paint_bytes(paint: &Paint) -> usize {
|
||||
/// About the bytes one copy of `paint` takes, with the strings it owns (swatch and pattern names).
|
||||
pub(crate) fn paint_bytes(paint: &Paint) -> usize {
|
||||
use std::mem::size_of;
|
||||
use vectorcraft_doc::color::{FreeformPoint, GradientStop};
|
||||
let own = size_of::<Paint>();
|
||||
let Paint::Gradient(g) = paint else { return own };
|
||||
let name = |s: &Option<String>| s.as_ref().map_or(0, String::len);
|
||||
let g = match paint {
|
||||
Paint::Solid { swatch, .. } => return own + name(swatch),
|
||||
Paint::Pattern { pattern, .. } => return own + pattern.len(),
|
||||
Paint::Gradient(g) => g,
|
||||
_ => return own,
|
||||
};
|
||||
let freeform = g
|
||||
.freeform
|
||||
.as_ref()
|
||||
.map_or(0, |f| f.points.len() * size_of::<FreeformPoint>() + f.lines.iter().map(|l| l.len() * size_of::<usize>()).sum::<usize>());
|
||||
own + size_of::<vectorcraft_doc::color::GradientPaint>() + g.gradient.stops.len() * size_of::<GradientStop>() + freeform
|
||||
let stops: usize = g.gradient.stops.iter().map(|s| size_of::<GradientStop>() + name(&s.swatch)).sum();
|
||||
own + size_of::<vectorcraft_doc::color::GradientPaint>() + name(&g.swatch) + stops + freeform
|
||||
}
|
||||
|
||||
/// Whether `copies` copies of `paint` stay within [`MAX_PAINT_COPIES`].
|
||||
pub(crate) fn paint_copies_fit(copies: usize, paint: &Paint) -> bool {
|
||||
copies.saturating_mul(paint_bytes(paint)) <= MAX_PAINT_COPIES
|
||||
}
|
||||
|
||||
fn visible(fx: &[Effect]) -> bool {
|
||||
@@ -112,7 +124,7 @@ fn surface(
|
||||
let colored = options.light_color != [1.0; 3] || options.lights.is_some();
|
||||
let faces = vectorcraft_three_d::revolve(path, b, options).ok()?;
|
||||
// A face's fill, its shadow and their seam strokes: up to four copies of the paint.
|
||||
if faces.len().saturating_mul(4).saturating_mul(paint_bytes(&paint)) > MAX_PAINT_COPIES {
|
||||
if !paint_copies_fit(faces.len().saturating_mul(4), &paint) {
|
||||
return None;
|
||||
}
|
||||
// Visibility is an expansion option, never a change to live geometry or export baking.
|
||||
|
||||
@@ -260,6 +260,10 @@ fn art_for(n: &Node, fx: &Effect, path: &PathData, rule: FillRule, items: &[&App
|
||||
let (paint, opacity, blend) = paint_of(item);
|
||||
let (area, rule) = region(path, rule, item);
|
||||
let f = facets(ROTATE, &area, rule, b, &p).ok()?;
|
||||
// Each facet and its seam stroke copy the paint; past the allowance the source path is drawn.
|
||||
if !crate::revolve::paint_copies_fit(f.len().saturating_mul(2), &paint) {
|
||||
return None;
|
||||
}
|
||||
out.push(Arc::new(surface(n, f, &paint, opacity, blend, colored)));
|
||||
}
|
||||
return Some(out);
|
||||
@@ -288,6 +292,7 @@ fn art_for(n: &Node, fx: &Effect, path: &PathData, rule: FillRule, items: &[&App
|
||||
for ((item, _, _), f) in parts.iter().zip(built) {
|
||||
let item: &AppearanceItem = item;
|
||||
match f {
|
||||
Ok(f) if !crate::revolve::paint_copies_fit(f.len().saturating_mul(2), &paint_of(item).0) => return None,
|
||||
Ok(f) => solids.push((item, f)),
|
||||
// A stroke's outline past the limits is left out; the fill's solid still draws.
|
||||
Err(_) if matches!(item, AppearanceItem::Stroke(_)) => {}
|
||||
|
||||
@@ -14,6 +14,10 @@ use super::{ARCLEN_ACCURACY, kink, push_seg, segments, subpaths, tangent};
|
||||
|
||||
/// More dashes than this (a tiny pattern on a long path) draws the line solid instead.
|
||||
const MAX_DASHES: f64 = 200_000.0;
|
||||
/// Pattern entries walked in all (every subpath and every corner restart starts the pattern over, and
|
||||
/// zero-length entries cost no path length, so a long pattern is walked in full even on a short
|
||||
/// run); past this the line is drawn solid. About 100x what real art needs.
|
||||
const MAX_STEPS: f64 = 20_000_000.0;
|
||||
/// A pattern whose longest entry is shorter than this can't advance along the path (each entry
|
||||
/// ends within the generator's tolerance of where it starts), so it draws the line solid.
|
||||
const MIN_ENTRY: f64 = 1e-6;
|
||||
@@ -72,6 +76,10 @@ pub fn dash(bp: &BezPath, d: &Dash) -> Option<Dashed> {
|
||||
if plans.iter().map(|p| p.periods).sum::<f64>() * pat.len() as f64 > MAX_DASHES {
|
||||
return None;
|
||||
}
|
||||
let restarts = |p: &Plan| p.restart.iter().filter(|r| **r).count() as f64;
|
||||
if plans.iter().map(|p| (p.periods.ceil().max(1.0) + restarts(p)) * pat.len() as f64).sum::<f64>() > MAX_STEPS {
|
||||
return None;
|
||||
}
|
||||
// Where an exact pattern starts: `offset` into it.
|
||||
let mut off = d.offset.rem_euclid(period);
|
||||
let mut ix0 = 0;
|
||||
|
||||
@@ -222,3 +222,17 @@ fn smooth_points_of_curves_stay_smooth_with_any_join() {
|
||||
assert!((a - want).abs() / want < 0.005, "{join:?}: {a} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_pattern_restarted_on_many_short_subpaths_draws_solid() {
|
||||
// Each subpath walks the zero-length entries in full although it is far shorter than a period.
|
||||
let mut pattern = vec![0.0; 2000];
|
||||
pattern[0] = 1.0;
|
||||
let mut b = BezPath::new();
|
||||
for i in 0..20_000 {
|
||||
b.move_to((i as f64 * 2.0, 0.0));
|
||||
b.line_to((i as f64 * 2.0 + 0.01, 0.0));
|
||||
}
|
||||
let d = Dash { pattern, offset: 0.0, align_corners: false };
|
||||
assert!(dash(&b, &d).is_none());
|
||||
}
|
||||
|
||||
@@ -218,7 +218,9 @@ fn visible(sources: &[TrapSource]) -> Vec<PathData> {
|
||||
/// The pairs `(i, j)`, `i < j`, in ascending order, of `rects` (`None`: left out) that come within
|
||||
/// `reach_x` of each other across x and `reach_y` across y. A sweep over the rects sorted by their
|
||||
/// left edge, so shapes spread over the page cost about one check each rather than one per pair.
|
||||
fn near_pairs(rects: &[Option<Rect>], reach_x: f64, reach_y: f64) -> Vec<(usize, usize)> {
|
||||
/// `None` when there are more than [`MAX_NEAR_PAIRS`] (many shapes all within reach of each other
|
||||
/// make the pair list grow with the square of their count).
|
||||
fn near_pairs(rects: &[Option<Rect>], reach_x: f64, reach_y: f64) -> Option<Vec<(usize, usize)>> {
|
||||
let mut order: Vec<(usize, Rect)> = rects.iter().enumerate().filter_map(|(i, r)| r.map(|r| (i, r))).collect();
|
||||
order.sort_unstable_by(|a, b| a.1.x0.total_cmp(&b.1.x0).then(a.0.cmp(&b.0)));
|
||||
let mut pairs = vec![];
|
||||
@@ -226,14 +228,21 @@ fn near_pairs(rects: &[Option<Rect>], reach_x: f64, reach_y: f64) -> Vec<(usize,
|
||||
// Later rects start at or right of `a`, so only how far right they start can rule them out.
|
||||
for &(j, b) in order[k + 1..].iter().take_while(|(_, b)| b.x0 <= a.x1 + reach_x) {
|
||||
if b.y0 <= a.y1 + reach_y && a.y0 <= b.y1 + reach_y {
|
||||
if pairs.len() >= MAX_NEAR_PAIRS {
|
||||
return None;
|
||||
}
|
||||
pairs.push((i.min(j), i.max(j)));
|
||||
}
|
||||
}
|
||||
}
|
||||
pairs.sort_unstable();
|
||||
pairs
|
||||
Some(pairs)
|
||||
}
|
||||
|
||||
/// Most candidate pairs one trap evaluation keeps; past it no traps are made (the art draws as is).
|
||||
/// Each pair costs a boolean trap band, so real art stays far below this.
|
||||
const MAX_NEAR_PAIRS: usize = 1 << 20;
|
||||
|
||||
/// The traps between `sources` (back to front) with options `o`: for each two sources of
|
||||
/// different solid colours whose visible parts meet, the strip of the darker one's visible part
|
||||
/// next to the lighter one's (the other way round with [`TrapOptions::reverse`]), painted per
|
||||
@@ -257,7 +266,8 @@ pub fn traps(sources: &[TrapSource], o: &TrapOptions) -> Vec<TrapPiece> {
|
||||
let extent = rects.iter().flatten().flat_map(|b| [b.x0, b.y0, b.x1, b.y1]).fold(0.0_f64, |m, v| m.max(v.abs()));
|
||||
let hair = 1e-9 * (1.0 + extent + w * r.max(1.0));
|
||||
let mut out = vec![];
|
||||
for (i, j) in near_pairs(&rects, w + hair, w * r + hair) {
|
||||
let Some(pairs) = near_pairs(&rects, w + hair, w * r + hair) else { return vec![] };
|
||||
for (i, j) in pairs {
|
||||
let (Some(ci), Some(cj)) = (colors[i], colors[j]) else { continue };
|
||||
if vis[i].is_empty() || vis[j].is_empty() || same_color(&sources[i].fill, &sources[j].fill, ci, cj) {
|
||||
continue;
|
||||
@@ -431,10 +441,13 @@ mod tests {
|
||||
fn only_shapes_within_reach_are_paired() {
|
||||
let s = many_apart_and_one_abutting();
|
||||
let rects: Vec<Option<Rect>> = s.iter().map(|t| t.path.bounds()).collect();
|
||||
assert_eq!(near_pairs(&rects, 1.0, 1.0), vec![(0, 2000)], "the squares 10 pt apart are never paired");
|
||||
assert_eq!(near_pairs(&rects, 1.0, 1.0), Some(vec![(0, 2000)]), "the squares 10 pt apart are never paired");
|
||||
// Reaching 10 pt, neighbours in a row pair, in ascending order; left-out rects never do.
|
||||
let row: Vec<Option<Rect>> = [3, 1, 0, 2].iter().map(|&k| s[k].path.bounds()).chain([None]).collect();
|
||||
assert_eq!(near_pairs(&row, 10.0, 10.0), vec![(0, 3), (1, 2), (1, 3)]);
|
||||
assert_eq!(near_pairs(&row, 10.0, 10.0), Some(vec![(0, 3), (1, 2), (1, 3)]));
|
||||
// Past the allowance the pair list stops growing instead of being built.
|
||||
let packed = vec![Some(Rect::new(0.0, 0.0, 1.0, 1.0)); 1500];
|
||||
assert_eq!(near_pairs(&packed, 1.0, 1.0), None);
|
||||
let t = traps(&s, &TrapOptions { thickness: 1.0, ..Default::default() });
|
||||
assert_eq!(t.len(), 1, "only the abutting pair traps");
|
||||
assert_eq!((t[0].from, t[0].into), (2000, 0));
|
||||
|
||||
Reference in New Issue
Block a user