mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 01:20:59 +00:00
Merge pull request #5302 from storytold/fix/sec-affinity-master-copy-cost
Affinity import: count path segments and masks in master page copies; range-check vector masks
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use super::nodes::MAX_DEPTH;
|
||||
use super::{Kind, af};
|
||||
|
||||
/// What copying a subtree takes: its nodes, characters and table cells (`work`), and its pixels.
|
||||
/// What copying a subtree takes: its nodes, characters, table cells and path segments (`work`),
|
||||
/// and its pixels.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(super) struct CopyCost {
|
||||
pub(super) work: u64,
|
||||
@@ -40,8 +41,58 @@ pub(super) fn copy_cost(n: &af::Node, depth: usize) -> CopyCost {
|
||||
let own = match &n.kind {
|
||||
Kind::Text(t) => CopyCost { work: 1 + len(&t.runs), pixels: 0 },
|
||||
Kind::Table(t) => CopyCost { work: 1 + t.cells.iter().map(|c| 1 + len(&c.runs)).sum::<u64>(), pixels: 0 },
|
||||
Kind::Image(img) => CopyCost { work: 1, pixels: u64::from(img.width) * u64::from(img.height) },
|
||||
Kind::Image(img) => CopyCost { work: 1, pixels: pixels(img) },
|
||||
// Each copy builds its own path: a curve of many segments costs them all.
|
||||
Kind::Shape { path, .. } | Kind::Artboard { path, .. } => CopyCost { work: 1 + segments(path), pixels: 0 },
|
||||
_ => CopyCost { work: 1, pixels: 0 },
|
||||
};
|
||||
n.children.iter().fold(own, |c, ch| c.plus(copy_cost(ch, depth + 1)))
|
||||
// A vector mask is a path of its own, and a pixel mask an image of its own, in every copy.
|
||||
let mask =
|
||||
CopyCost { work: n.mask.as_ref().map_or(0, segments) + u64::from(n.pixel_mask.is_some()), pixels: n.pixel_mask.as_ref().map_or(0, pixels) };
|
||||
n.children.iter().fold(own.plus(mask), |c, ch| c.plus(copy_cost(ch, depth + 1)))
|
||||
}
|
||||
|
||||
/// The subpaths and segments of `p`.
|
||||
fn segments(p: &af::Path) -> u64 {
|
||||
p.subpaths.iter().map(|s| 1 + s.segments.len() as u64).sum()
|
||||
}
|
||||
|
||||
fn pixels(img: &af::Image) -> u64 {
|
||||
u64::from(img.width) * u64::from(img.height)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn node(kind: Kind, mask: Option<af::Path>) -> af::Node {
|
||||
af::Node {
|
||||
class: vectorcraft_affinity::stream::Tag::of(b"ShpN"),
|
||||
name: String::new(),
|
||||
visible: true,
|
||||
locked: false,
|
||||
opacity: 1.0,
|
||||
blend: af::Blend::Normal,
|
||||
kind,
|
||||
mask,
|
||||
pixel_mask: None,
|
||||
effects: vec![],
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn path(n: usize) -> af::Path {
|
||||
let p = af::Point { x: 0.0, y: 0.0 };
|
||||
af::Path { subpaths: vec![af::SubPath { start: p, segments: vec![[p; 3]; n], closed: false }] }
|
||||
}
|
||||
|
||||
/// A curve, and a vector mask, cost every segment each copy rebuilds: a shape of a million
|
||||
/// segments shown on many pages can't pass as one object.
|
||||
#[test]
|
||||
fn paths_and_masks_cost_their_segments() {
|
||||
let shape = |n| Kind::Shape { path: path(n), fills: vec![], strokes: vec![], even_odd: false };
|
||||
assert!(copy_cost(&node(shape(1_000_000), None), 0).work > 1_000_000);
|
||||
assert!(copy_cost(&node(Kind::Group, Some(path(1_000_000))), 0).work > 1_000_000);
|
||||
assert!(copy_cost(&node(shape(3), None), 0).work < 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@ pub(super) fn affine(a: af::Affine) -> Affine {
|
||||
Affine::new(a.0)
|
||||
}
|
||||
|
||||
/// Are all of `p`'s points, mapped by `to_doc`, finite and within `MAX_COORD` of the origin?
|
||||
pub(super) fn path_in_range(p: &af::Path, to_doc: Affine) -> bool {
|
||||
let ok = |q: &af::Point| {
|
||||
let d = to_doc * kurbo::Point::new(q.x, q.y);
|
||||
[d.x, d.y].iter().all(|v| v.is_finite() && v.abs() <= crate::MAX_COORD)
|
||||
};
|
||||
p.subpaths.iter().all(|s| ok(&s.start) && s.segments.iter().flatten().all(ok))
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
pub(super) fn layer_with(&mut self, name: Option<&str>, children: Vec<Arc<Node>>) -> Arc<Node> {
|
||||
let id = self.doc.alloc_id();
|
||||
@@ -107,6 +116,12 @@ impl Builder {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// So must its vector mask, which becomes a clipping path: every control point, not just
|
||||
// the finite ones a bounding box takes.
|
||||
if n.mask.as_ref().is_some_and(|m| !path_in_range(m, to_doc)) {
|
||||
self.warn("objects beyond the document range VectorCraft supports (left out)");
|
||||
return None;
|
||||
}
|
||||
let n = &*self.overlaid(n);
|
||||
// Register before descending so nested artboards keep their document order. Inside an
|
||||
// embedded document an artboard is just its content's frame.
|
||||
@@ -252,3 +267,25 @@ impl Builder {
|
||||
Some(Arc::new(out))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn path(points: &[(f64, f64)]) -> af::Path {
|
||||
let p = |&(x, y): &(f64, f64)| af::Point { x, y };
|
||||
let start = p(&points[0]);
|
||||
let segments = points[1..].iter().map(|q| [start, p(q), p(q)]).collect();
|
||||
af::Path { subpaths: vec![af::SubPath { start, segments, closed: true }] }
|
||||
}
|
||||
|
||||
/// A mask's control points are checked as mapped into the document, NaN ones included.
|
||||
#[test]
|
||||
fn mask_paths_must_stay_in_the_document_range() {
|
||||
let k = Affine::scale(0.75);
|
||||
assert!(path_in_range(&path(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)]), k));
|
||||
assert!(!path_in_range(&path(&[(0.0, 0.0), (1.0e7, 0.0)]), k));
|
||||
assert!(!path_in_range(&path(&[(0.0, 0.0), (f64::NAN, 0.0)]), k));
|
||||
assert!(!path_in_range(&path(&[(0.0, 0.0), (1.0, 1.0)]), Affine::scale(1.0e9)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user