Fix #1705: a Transform effect's copies are bounded by the anchors they make
Each Transform effect clamps its own copies to 1000, but each copies all of what the effect before it made: three of them on one square ask for a thousand million squares, from a small file. Copies now stop where the result would pass two million anchors (1000 copies of a 2000-anchor path).
This commit is contained in:
parent
48041c8cd3
commit
779120cb5b
|
|
@ -108,6 +108,10 @@ const MAX_SCALE: f64 = 100_000.0;
|
||||||
const MAX_MOVE: f64 = 1.0e7;
|
const MAX_MOVE: f64 = 1.0e7;
|
||||||
/// The Transform effect's copies stop before one reaches further than this (pt) from the origin.
|
/// The Transform effect's copies stop before one reaches further than this (pt) from the origin.
|
||||||
const MAX_REACH: f64 = 1.0e9;
|
const MAX_REACH: f64 = 1.0e9;
|
||||||
|
/// Most anchors a Transform effect's copies make of a path, the original included: 1000 copies of
|
||||||
|
/// a 2000-anchor path. Stacked Transform effects multiply their copies (three of 1000 copies make
|
||||||
|
/// a thousand million of one square), so the limit is on the result, not on each effect's count.
|
||||||
|
pub(crate) const MAX_COPY_ANCHORS: usize = 2_000_000;
|
||||||
|
|
||||||
/// Transform effect: scale, rotate and reflect about reference point `reference` of the bounds
|
/// Transform effect: scale, rotate and reflect about reference point `reference` of the bounds
|
||||||
/// (the 9-point grid, 4 = centre), then move; `copies` more times, each from the last. With
|
/// (the 9-point grid, 4 = centre), then move; `copies` more times, each from the last. With
|
||||||
|
|
@ -115,6 +119,11 @@ const MAX_REACH: f64 = 1.0e9;
|
||||||
/// the angle a share of theirs, drawn from `seed` (the object's), so an object keeps its result
|
/// the angle a share of theirs, drawn from `seed` (the object's), so an object keeps its result
|
||||||
/// on every redraw while each object varies its own way.
|
/// on every redraw while each object varies its own way.
|
||||||
pub fn transform(path: &PathData, b: Rect, p: &Value, seed: u64) -> PathData {
|
pub fn transform(path: &PathData, b: Rect, p: &Value, seed: u64) -> PathData {
|
||||||
|
transform_within(path, b, p, seed, MAX_COPY_ANCHORS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`transform`] whose copies make at most `max` anchors of the path, the original included.
|
||||||
|
pub(crate) fn transform_within(path: &PathData, b: Rect, p: &Value, seed: u64, max: usize) -> PathData {
|
||||||
let random = flag(p, "random", false);
|
let random = flag(p, "random", false);
|
||||||
// The share (0..1) of value `k` applied.
|
// The share (0..1) of value `k` applied.
|
||||||
let share = |k: u64| if random { (noise(seed, k, 0, 0) + 1.0) / 2.0 } else { 1.0 };
|
let share = |k: u64| if random { (noise(seed, k, 0, 0) + 1.0) / 2.0 } else { 1.0 };
|
||||||
|
|
@ -132,9 +141,17 @@ pub fn transform(path: &PathData, b: Rect, p: &Value, seed: u64) -> PathData {
|
||||||
return path.transformed(m);
|
return path.transformed(m);
|
||||||
}
|
}
|
||||||
let within = |r: Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.abs() <= MAX_REACH);
|
let within = |r: Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.abs() <= MAX_REACH);
|
||||||
|
// Each copy adds the path once more, and a Transform after this one copies the lot again:
|
||||||
|
// copies stop where the result would pass [`MAX_COPY_ANCHORS`] (#1705).
|
||||||
|
let each = path.anchor_count();
|
||||||
|
let mut anchors = each;
|
||||||
let mut out = path.clone();
|
let mut out = path.clone();
|
||||||
let mut cur = path.clone();
|
let mut cur = path.clone();
|
||||||
for _ in 0..copies {
|
for _ in 0..copies {
|
||||||
|
anchors = anchors.saturating_add(each);
|
||||||
|
if anchors > max {
|
||||||
|
break;
|
||||||
|
}
|
||||||
cur = cur.transformed(m);
|
cur = cur.transformed(m);
|
||||||
if !cur.bounds().is_some_and(within) {
|
if !cur.bounds().is_some_and(within) {
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,24 @@ fn random_transform_is_stable_per_object_and_within_the_values() {
|
||||||
assert!(close(r.width(), 50.0, 1e-9) && close(r.center().x, 90.0, 1e-9), "{r:?}");
|
assert!(close(r.width(), 50.0, 1e-9) && close(r.center().x, 90.0, 1e-9), "{r:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #1705: stacked Transform effects multiply their copies; the anchors they make are bounded,
|
||||||
|
/// whatever each one's count.
|
||||||
|
#[test]
|
||||||
|
fn stacked_transform_copies_stay_within_the_anchor_budget() {
|
||||||
|
let p = json!({"copies": 1000});
|
||||||
|
let b = square().bounds().unwrap();
|
||||||
|
let mut path = square();
|
||||||
|
let mut counts = vec![];
|
||||||
|
for _ in 0..3 {
|
||||||
|
path = crate::distort::transform_within(&path, b, &p, 0, 100);
|
||||||
|
counts.push(path.anchor_count());
|
||||||
|
}
|
||||||
|
assert_eq!(counts, [100, 100, 100], "25 squares, then no room for a copy of them");
|
||||||
|
// Within the budget every copy is made, and the effect's own budget takes a thousand copies.
|
||||||
|
assert_eq!(crate::distort::transform_within(&square(), b, &json!({"copies": 24}), 0, 100).subpaths.len(), 25);
|
||||||
|
assert_eq!(run("distort.transform", p, &square()).subpaths.len(), 1001);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transform_copies_stop_before_they_overflow() {
|
fn transform_copies_stop_before_they_overflow() {
|
||||||
let out = run("distort.transform", json!({"scaleH": 100000, "scaleV": 100000, "copies": 1000}), &square());
|
let out = run("distort.transform", json!({"scaleH": 100000, "scaleV": 100000, "copies": 1000}), &square());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue