mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
effects: a Transform effect's image copies stay within a node budget (#2042)
This commit is contained in:
committed by
bflatastic
parent
ff79f332a7
commit
d5abc71677
@@ -112,6 +112,11 @@ const MAX_REACH: f64 = 1.0e9;
|
||||
/// 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;
|
||||
/// Most image nodes a Transform effect's copies make of an art, the originals included: 1000
|
||||
/// copies of a 2000-image art. Stacked Transform effects multiply their copies (three of 1000
|
||||
/// copies make a thousand million of one image), so the limit is on the result, not on each
|
||||
/// effect's count.
|
||||
pub(crate) const MAX_COPY_IMAGES: usize = 2_000_000;
|
||||
|
||||
/// 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
|
||||
@@ -152,7 +157,8 @@ pub(crate) fn transform_within(path: &PathData, b: Rect, p: &Value, seed: u64, m
|
||||
/// The Transform effect's map and how many copies of what it is applied to it makes, for bounds
|
||||
/// `b` (the reference point lies on them), params `p` and the object's `seed` for Random: the map
|
||||
/// applies to the object itself when `copies` is 0, and once more to each of `copies` copies
|
||||
/// otherwise (see [`transform_within`], and images, which have no anchors to budget).
|
||||
/// otherwise (see [`transform_within`], and images, whose copies count nodes against
|
||||
/// [`MAX_COPY_IMAGES`]).
|
||||
pub(crate) fn transform_map(b: Rect, p: &Value, seed: u64) -> (Affine, usize) {
|
||||
let random = flag(p, "random", false);
|
||||
// The share (0..1) of value `k` applied.
|
||||
|
||||
@@ -222,10 +222,24 @@ pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect]) {
|
||||
/// images follow, each mapped once more than the last. Without copies the image itself takes the
|
||||
/// map, as a path does.
|
||||
fn transform_images(n: &mut Node, m: Affine, copies: usize) {
|
||||
transform_images_within(n, m, copies, crate::distort::MAX_COPY_IMAGES)
|
||||
}
|
||||
|
||||
/// [`transform_images`] whose copies make at most `max` image nodes of the art, the originals
|
||||
/// included: each copy adds the image once more, and a Transform after this one copies the lot
|
||||
/// again, so copies stop where the result would pass `max` (#2042).
|
||||
pub(crate) fn transform_images_within(n: &mut Node, m: Affine, copies: usize, max: usize) {
|
||||
let mut room = max.saturating_sub(image_count(n));
|
||||
copy_images(n, m, copies, &mut room);
|
||||
}
|
||||
|
||||
/// The walk of [`transform_images`]: the map and its copies go to each image in document order,
|
||||
/// each copy taking one from `room` (what the art can still grow by), until the budget is full.
|
||||
fn copy_images(n: &mut Node, m: Affine, copies: usize, room: &mut usize) {
|
||||
let Some(children) = n.children_mut() else { return };
|
||||
for c in children.iter_mut() {
|
||||
if !matches!(c.kind, NodeKind::Image(_)) {
|
||||
transform_images(Arc::make_mut(c), m, copies);
|
||||
copy_images(Arc::make_mut(c), m, copies, room);
|
||||
continue;
|
||||
}
|
||||
let src = (**c).clone();
|
||||
@@ -240,12 +254,14 @@ fn transform_images(n: &mut Node, m: Affine, copies: usize) {
|
||||
*c = Arc::new(placed(m));
|
||||
continue;
|
||||
}
|
||||
let made = copies.min(*room);
|
||||
*room -= made;
|
||||
// The copies become image nodes of their own, under a group with the image's id, name and
|
||||
// place in the art (as the runs an outlined text gets).
|
||||
let mut images: Vec<Arc<Node>> = Vec::with_capacity(copies + 1);
|
||||
let mut images: Vec<Arc<Node>> = Vec::with_capacity(made + 1);
|
||||
images.push(Arc::new(placed(Affine::IDENTITY)));
|
||||
let mut xf = Affine::IDENTITY;
|
||||
for _ in 0..copies {
|
||||
for _ in 0..made {
|
||||
xf *= m;
|
||||
images.push(Arc::new(placed(xf)));
|
||||
}
|
||||
@@ -255,6 +271,11 @@ fn transform_images(n: &mut Node, m: Affine, copies: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many image nodes `n` holds below itself.
|
||||
fn image_count(n: &Node) -> usize {
|
||||
n.children().into_iter().flatten().map(|c| usize::from(matches!(c.kind, NodeKind::Image(_))) + image_count(c)).sum()
|
||||
}
|
||||
|
||||
/// `n` with type anywhere inside it outlined and live objects evaluated, so geometry effects on
|
||||
/// it reach every member.
|
||||
pub(crate) fn outlined_members(n: &Node) -> Node {
|
||||
|
||||
@@ -234,6 +234,49 @@ fn stacked_transform_copies_stay_within_the_anchor_budget() {
|
||||
assert_eq!(run("distort.transform", p, &square()).subpaths.len(), 1001);
|
||||
}
|
||||
|
||||
/// #2042: stacked Transform effects multiply their image copies as they do their anchors; the
|
||||
/// image nodes they make are bounded, whatever each one's count.
|
||||
#[test]
|
||||
fn stacked_transform_copies_stay_within_the_image_budget() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{ImageObject, NodeId};
|
||||
let id = NodeId(9);
|
||||
let image = || {
|
||||
Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "k".into(),
|
||||
width: 40,
|
||||
height: 40,
|
||||
xf: Affine::translate((10.0, 0.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
})),
|
||||
)
|
||||
};
|
||||
let count = |art: &Node| {
|
||||
let mut xs = vec![];
|
||||
image_xs(art, &mut xs);
|
||||
xs.len()
|
||||
};
|
||||
let m = Affine::translate((70.0, 0.0));
|
||||
let mut art = Node::group(id, vec![Arc::new(image())]);
|
||||
let mut counts = vec![];
|
||||
for _ in 0..3 {
|
||||
crate::reshape::transform_images_within(&mut art, m, 1000, 100);
|
||||
counts.push(count(&art));
|
||||
}
|
||||
assert_eq!(counts, [100, 100, 100], "99 copies, 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.
|
||||
let mut art = Node::group(id, vec![Arc::new(image())]);
|
||||
crate::reshape::transform_images_within(&mut art, m, 24, 100);
|
||||
assert_eq!(count(&art), 25);
|
||||
let mut g = Node::group(id, vec![Arc::new(image())]);
|
||||
g.appearance.effects.push(fx("distort.transform", json!({"moveH": 70, "copies": 1000})));
|
||||
assert_eq!(count(&evaluate_container(&g).unwrap()), 1001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_copies_stop_before_they_overflow() {
|
||||
let out = run("distort.transform", json!({"scaleH": 100000, "scaleV": 100000, "copies": 1000}), &square());
|
||||
@@ -268,14 +311,20 @@ fn transform_on_a_group_moves_and_copies_its_images() {
|
||||
xs.sort_by(f64::total_cmp);
|
||||
assert_eq!(xs, [10.0, 80.0, 150.0], "the image and its two copies, 70 pt apart");
|
||||
// Without copies the image itself takes the transform, as a path does.
|
||||
let mut g = Node::group(id, vec![Arc::new(Node::new(id, NodeKind::Image(Box::new(ImageObject {
|
||||
key: "k".into(),
|
||||
width: 40,
|
||||
height: 40,
|
||||
xf: Affine::translate((10.0, 0.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
}))))]);
|
||||
let mut g = Node::group(
|
||||
id,
|
||||
vec![Arc::new(Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "k".into(),
|
||||
width: 40,
|
||||
height: 40,
|
||||
xf: Affine::translate((10.0, 0.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
})),
|
||||
))],
|
||||
);
|
||||
g.appearance.effects.push(fx("distort.transform", json!({"scaleH": 50, "scaleV": 50, "reference": 8})));
|
||||
let art = evaluate_container(&g).unwrap();
|
||||
let mut xs = vec![];
|
||||
|
||||
Reference in New Issue
Block a user