mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
One pattern fill tiles at most 250,000 times (PatternDef::offsets_covering), but Scene::fill writes each tile's own pattern fills again, down to the nesting limit of 8, with nothing bounding the total. A pattern whose tile art is filled with another pattern (or with itself) multiplies the tile count at every level: a small document makes EMF export (the engine's "emf" format, also reachable over MCP and the control channel) run out of memory or never finish, and the unsaved document is lost with it. The scene now keeps one tile budget for the whole walk. A fill whose tiles don't fit in what is left, and every pattern fill after it, is written as one colour like WMF writes patterns, with a warning. The test tiles squares filled with dots (over half a million fills before) and a pattern filled with itself, and checks both stay under the budget. Reported-by: BS.ehlas.cz
376 lines
17 KiB
Rust
376 lines
17 KiB
Rust
//! EMF and WMF output read back record by record: headers, record sizes, paths, pens, brushes,
|
||
//! clips, images, and what each format leaves out.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_color::{Color, Gradient, GradientPaint, Paint};
|
||
use vectorcraft_doc::{
|
||
Appearance, CharStyle, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, PatternDef, TextObject,
|
||
};
|
||
use vectorcraft_geom::{Affine, PathData, Point, Rect, shapes};
|
||
|
||
use crate::*;
|
||
|
||
/// Add `n` (with a fresh id) on top of `d`'s first layer.
|
||
pub(crate) fn add(d: &mut Document, mut n: Node) {
|
||
n.id = d.alloc_id();
|
||
let layer = d.layers[0].id;
|
||
let at = d.layers[0].children().map_or(0, Vec::len);
|
||
d.insert(Some(layer), at, n).unwrap();
|
||
}
|
||
|
||
/// A document of one `w` × `h` artboard holding `nodes` on its layer.
|
||
pub(crate) fn doc_with(w: f64, h: f64, nodes: Vec<Node>) -> Document {
|
||
let mut d = Document::new(w, h);
|
||
nodes.into_iter().for_each(|n| add(&mut d, n));
|
||
d
|
||
}
|
||
|
||
pub(crate) fn path(p: PathData, fill: Paint, stroke: Paint, width: f64) -> Node {
|
||
Node::path(NodeId(0), p, Appearance::basic(fill, stroke, width))
|
||
}
|
||
|
||
pub(crate) fn red() -> Paint {
|
||
Paint::solid(Color::rgb8(255, 0, 0))
|
||
}
|
||
|
||
pub(crate) fn blue() -> Paint {
|
||
Paint::solid(Color::rgb8(0, 0, 255))
|
||
}
|
||
|
||
/// A `w` × `h` PNG of `rgba`.
|
||
pub(crate) fn png(w: u32, h: u32, rgba: [u8; 4]) -> Vec<u8> {
|
||
let img = image::RgbaImage::from_pixel(w, h, image::Rgba(rgba));
|
||
let mut out = vec![];
|
||
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
|
||
out
|
||
}
|
||
|
||
/// An image node of `bytes` placed by `xf`, its blob added to `d`.
|
||
pub(crate) fn image_node(d: &mut Document, bytes: Vec<u8>, w: u32, h: u32, xf: Affine) -> Node {
|
||
let blob = ImageBlob::new("image/png", bytes);
|
||
let key = blob.content_key();
|
||
d.images.insert(key.clone(), blob);
|
||
Node::new(NodeId(0), NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })))
|
||
}
|
||
|
||
pub(crate) fn u32_at(b: &[u8], at: usize) -> u32 {
|
||
u32::from_le_bytes(b[at..at + 4].try_into().unwrap())
|
||
}
|
||
|
||
pub(crate) fn i32_at(b: &[u8], at: usize) -> i32 {
|
||
i32::from_le_bytes(b[at..at + 4].try_into().unwrap())
|
||
}
|
||
|
||
pub(crate) fn u16_at(b: &[u8], at: usize) -> u16 {
|
||
u16::from_le_bytes(b[at..at + 2].try_into().unwrap())
|
||
}
|
||
|
||
/// The EMF's records: (type, the whole record).
|
||
pub(crate) fn emf_records(b: &[u8]) -> Vec<(u32, &[u8])> {
|
||
let mut out = vec![];
|
||
let mut at = 0;
|
||
while at < b.len() {
|
||
let size = u32_at(b, at + 4) as usize;
|
||
assert!(size >= 8 && size.is_multiple_of(4), "record size {size} at {at}");
|
||
out.push((u32_at(b, at), &b[at..at + size]));
|
||
at += size;
|
||
}
|
||
assert_eq!(at, b.len(), "records end at the end of the file");
|
||
out
|
||
}
|
||
|
||
/// The WMF's records after both headers: (function, the whole record).
|
||
pub(crate) fn wmf_records(b: &[u8]) -> Vec<(u16, &[u8])> {
|
||
let mut out = vec![];
|
||
let mut at = 22 + 18;
|
||
while at < b.len() {
|
||
let words = u32_at(b, at) as usize;
|
||
assert!(words >= 3, "record of {words} words at {at}");
|
||
out.push((u16_at(b, at + 4), &b[at..at + words * 2]));
|
||
at += words * 2;
|
||
}
|
||
assert_eq!(at, b.len());
|
||
out
|
||
}
|
||
|
||
pub(crate) fn emf(d: &Document) -> Output {
|
||
export(d, d.artboards[0].rect, Kind::Emf).unwrap()
|
||
}
|
||
|
||
pub(crate) fn wmf(d: &Document) -> Output {
|
||
export(d, d.artboards[0].rect, Kind::Wmf).unwrap()
|
||
}
|
||
|
||
fn kinds(recs: &[(u32, &[u8])]) -> Vec<u32> {
|
||
recs.iter().map(|r| r.0).collect()
|
||
}
|
||
|
||
fn count(k: &[u32], kind: u32) -> usize {
|
||
k.iter().filter(|x| **x == kind).count()
|
||
}
|
||
|
||
#[test]
|
||
fn emf_header_frame_is_the_region_in_hundredths_of_a_millimetre() {
|
||
// 72 × 144 pt is 25.4 × 50.8 mm; a rectangle from (36, 36) to (54, 72) pt.
|
||
let d = doc_with(72.0, 144.0, vec![path(shapes::rectangle(Rect::new(36.0, 36.0, 54.0, 72.0)), red(), Paint::None, 0.0)]);
|
||
let b = &emf(&d).bytes;
|
||
assert_eq!(sniff(b), Some(Kind::Emf));
|
||
assert_eq!(u32_at(b, 0), 1);
|
||
assert_eq!(u32_at(b, 40), 0x464D_4520, "the \" EMF\" signature");
|
||
// rclFrame.
|
||
assert_eq!([i32_at(b, 24), i32_at(b, 28), i32_at(b, 32), i32_at(b, 36)], [0, 0, 2540, 5080]);
|
||
// rclBounds, in device units of 0.01 mm: the rectangle.
|
||
assert_eq!([i32_at(b, 8), i32_at(b, 12), i32_at(b, 16), i32_at(b, 20)], [1270, 1270, 1905, 2540]);
|
||
// The reference device has 100 pixels a millimetre.
|
||
let (dev, mm) = ((i32_at(b, 72), i32_at(b, 76)), (i32_at(b, 80), i32_at(b, 84)));
|
||
assert_eq!(dev, (mm.0 * 100, mm.1 * 100));
|
||
assert_eq!((u32_at(b, 100), u32_at(b, 104)), (mm.0 as u32 * 1000, mm.1 as u32 * 1000), "micrometres");
|
||
// The description names the app.
|
||
let n = u32_at(b, 60) as usize;
|
||
let off = u32_at(b, 64) as usize;
|
||
assert_eq!(off, 108);
|
||
let desc: Vec<u16> = (0..n).map(|i| u16_at(b, off + 2 * i)).collect();
|
||
assert!(String::from_utf16_lossy(&desc).starts_with("VectorCraft\0"));
|
||
// An empty picture has empty bounds.
|
||
let empty = emf(&doc_with(72.0, 72.0, vec![]));
|
||
assert_eq!([i32_at(&empty.bytes, 8), i32_at(&empty.bytes, 16)], [0, -1]);
|
||
}
|
||
|
||
#[test]
|
||
fn emf_record_sizes_add_up_to_the_header_totals() {
|
||
let mut d = doc_with(200.0, 100.0, vec![]);
|
||
let img = image_node(&mut d, png(3, 2, [0, 128, 0, 255]), 3, 2, Affine::translate((10.0, 10.0)) * Affine::scale(10.0));
|
||
add(&mut d, img);
|
||
add(&mut d, path(shapes::ellipse(Rect::new(50.0, 10.0, 150.0, 90.0)), red(), blue(), 2.0));
|
||
let b = emf(&d).bytes;
|
||
let recs = emf_records(&b);
|
||
assert_eq!(u32_at(&b, 48) as usize, b.len(), "nBytes");
|
||
assert_eq!(u32_at(&b, 52) as usize, recs.len(), "nRecords");
|
||
assert_eq!(u16_at(&b, 56), 3, "nHandles: the reserved slot, a brush and a pen");
|
||
let (last, eof) = recs.last().copied().unwrap();
|
||
assert_eq!((last, eof.len()), (14, 20), "EMR_EOF ends the file");
|
||
assert_eq!(u32_at(eof, 16), 20, "nSizeLast");
|
||
// Fixed-size records have their fixed sizes.
|
||
for (k, r) in &recs {
|
||
let fixed = match k {
|
||
18 | 19 | 34 | 37 | 40 | 58 | 67 => Some(12),
|
||
27 | 54 => Some(16),
|
||
33 | 59 | 60 | 61 => Some(8),
|
||
39 | 62 | 64 => Some(24),
|
||
35 => Some(32),
|
||
36 => Some(36),
|
||
_ => None,
|
||
};
|
||
if let Some(f) = fixed {
|
||
assert_eq!(r.len(), f, "record {k}");
|
||
}
|
||
}
|
||
// StretchDIBits: its bitmap where its offsets say, 3 × 2 pixels of 24 bits (rows of 12 bytes).
|
||
let sdib = recs.iter().find(|r| r.0 == 81).unwrap().1;
|
||
assert_eq!([u32_at(sdib, 48), u32_at(sdib, 52), u32_at(sdib, 56), u32_at(sdib, 60)], [80, 40, 120, 24]);
|
||
assert_eq!(sdib.len(), 144);
|
||
}
|
||
|
||
#[test]
|
||
fn emf_paths_keep_their_curves_brushes_and_pens() {
|
||
let mut e = path(shapes::ellipse(Rect::new(10.0, 10.0, 60.0, 40.0)), red(), blue(), 4.0);
|
||
if let Some(st) = e.appearance.stroke_mut() {
|
||
st.cap = LineCap::Round;
|
||
st.join = LineJoin::Bevel;
|
||
}
|
||
let b = emf(&doc_with(100.0, 100.0, vec![e])).bytes;
|
||
let recs = emf_records(&b);
|
||
let k = kinds(&recs);
|
||
assert!(k.contains(&5), "EMR_POLYBEZIERTO keeps the curves");
|
||
assert!(k.contains(&62) && k.contains(&64), "filled, then stroked");
|
||
let brush = recs.iter().find(|r| r.0 == 39).unwrap().1;
|
||
assert_eq!(u32_at(brush, 16), 0x0000_00ff, "a red COLORREF");
|
||
let pen = recs.iter().find(|r| r.0 == 95).unwrap().1;
|
||
let style = u32_at(pen, 28);
|
||
assert_eq!(style & 0x000f_0000, 0x0001_0000, "geometric");
|
||
assert_eq!(style & 0xf00, 0, "round caps");
|
||
assert_eq!(style & 0xf000, 0x1000, "bevel joins");
|
||
assert_eq!(u32_at(pen, 32), (4.0 * 2540.0 / 72.0f64).round() as u32, "4 pt in 0.01 mm");
|
||
assert_eq!(u32_at(pen, 40), 0x00ff_0000, "a blue COLORREF");
|
||
assert!(recs.iter().any(|r| r.0 == 19 && u32_at(r.1, 8) == 2), "WINDING for non-zero");
|
||
// One brush and one pen for two shapes of the same colours.
|
||
let two = doc_with(
|
||
100.0,
|
||
100.0,
|
||
vec![
|
||
path(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), red(), blue(), 1.0),
|
||
path(shapes::rectangle(Rect::new(20.0, 0.0, 30.0, 10.0)), red(), blue(), 1.0),
|
||
],
|
||
);
|
||
let k = kinds(&emf_records(&emf(&two).bytes));
|
||
assert_eq!((count(&k, 39), count(&k, 95)), (1, 1));
|
||
}
|
||
|
||
#[test]
|
||
fn emf_dashes_are_pen_styles() {
|
||
let mut l = path(shapes::line(Point::new(10.0, 10.0), Point::new(90.0, 10.0)), Paint::None, blue(), 2.0);
|
||
l.appearance.stroke_mut().unwrap().dash = Some(Dash { pattern: vec![6.0, 3.0], offset: 0.0, align_corners: false });
|
||
let b = emf(&doc_with(100.0, 100.0, vec![l.clone()])).bytes;
|
||
let recs = emf_records(&b);
|
||
let pen = recs.iter().find(|r| r.0 == 95).unwrap().1;
|
||
assert_eq!(u32_at(pen, 28) & 0xf, 7, "PS_USERSTYLE");
|
||
assert_eq!(u32_at(pen, 48), 2);
|
||
assert_eq!([u32_at(pen, 52), u32_at(pen, 56)], [(6.0 * 2540.0 / 72.0f64).round() as u32, (3.0 * 2540.0 / 72.0f64).round() as u32]);
|
||
// With an offset, the dashes are separate lines drawn with a solid pen.
|
||
l.appearance.stroke_mut().unwrap().dash = Some(Dash { pattern: vec![6.0, 3.0], offset: 2.0, align_corners: false });
|
||
let b = emf(&doc_with(100.0, 100.0, vec![l])).bytes;
|
||
let recs = emf_records(&b);
|
||
assert_eq!(u32_at(recs.iter().find(|r| r.0 == 95).unwrap().1, 28) & 0xf, 0);
|
||
assert!(count(&kinds(&recs), 27) > 5, "a move-to for each dash");
|
||
}
|
||
|
||
#[test]
|
||
fn emf_clips_gradients_and_images() {
|
||
let g = Paint::Gradient(Box::new(GradientPaint::new(Gradient::default())));
|
||
let mut d = doc_with(100.0, 100.0, vec![path(shapes::rectangle(Rect::new(10.0, 10.0, 60.0, 30.0)), g, Paint::None, 0.0)]);
|
||
let opaque = image_node(&mut d, png(4, 4, [10, 20, 30, 255]), 4, 4, Affine::translate((0.0, 50.0)));
|
||
let clear = image_node(&mut d, png(4, 4, [10, 20, 30, 100]), 4, 4, Affine::translate((50.0, 50.0)) * Affine::rotate(0.3));
|
||
add(&mut d, opaque);
|
||
add(&mut d, clear);
|
||
let out = emf(&d);
|
||
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
|
||
let recs = emf_records(&out.bytes);
|
||
let k = kinds(&recs);
|
||
// The region's clip, then the gradient's clip around its image.
|
||
assert_eq!(count(&k, 67), 2);
|
||
assert_eq!(count(&k, 33), count(&k, 34), "every save is restored");
|
||
// The (opaque) gradient's image and the opaque image are StretchDIBits; the translucent one
|
||
// AlphaBlend with per-pixel alpha.
|
||
assert_eq!(count(&k, 81), 2);
|
||
let ab = recs.iter().find(|r| r.0 == 114).unwrap().1;
|
||
assert_eq!(&ab[40..44], &[0, 0, 255, 1]);
|
||
// Images are placed by a world transform, reset after each.
|
||
assert_eq!((count(&k, 35), count(&k, 36)), (3, 3));
|
||
}
|
||
|
||
#[test]
|
||
fn type_is_written_as_outlines() {
|
||
let t = TextObject::point(Point::new(10.0, 50.0), "Hi", CharStyle { fill: red(), ..CharStyle::default() });
|
||
let d = doc_with(100.0, 100.0, vec![Node::new(NodeId(0), NodeKind::Text(Box::new(t)))]);
|
||
let k = kinds(&emf_records(&emf(&d).bytes));
|
||
assert!(k.contains(&62), "glyphs are filled paths");
|
||
assert!(!k.contains(&84) && !k.contains(&83), "no text records");
|
||
}
|
||
|
||
#[test]
|
||
fn wmf_placeable_header_checksum_and_record_sizes() {
|
||
let d = doc_with(72.0, 144.0, vec![path(shapes::ellipse(Rect::new(10.0, 10.0, 60.0, 40.0)), red(), blue(), 1.0)]);
|
||
let out = wmf(&d);
|
||
let b = &out.bytes;
|
||
assert_eq!(sniff(b), Some(Kind::Wmf));
|
||
assert_eq!(u32_at(b, 0), 0x9AC6_CDD7);
|
||
// The box in logical units at 1440 an inch: 1 × 2 inches.
|
||
assert_eq!([u16_at(b, 6), u16_at(b, 8), u16_at(b, 10), u16_at(b, 12)], [0, 0, 1440, 2880]);
|
||
assert_eq!(u16_at(b, 14), 1440);
|
||
let xor = (0..10).fold(0u16, |a, i| a ^ u16_at(b, 2 * i));
|
||
assert_eq!(u16_at(b, 20), xor, "the checksum");
|
||
assert_eq!(crate::wmf::checksum(&b[..20]), xor);
|
||
// The WMF header: a memory metafile of 9 words, its size in words, the largest record.
|
||
assert_eq!([u16_at(b, 22), u16_at(b, 24), u16_at(b, 26)], [1, 9, 0x0300]);
|
||
assert_eq!(u32_at(b, 28) as usize * 2, b.len() - 22);
|
||
let recs = wmf_records(b);
|
||
let largest = recs.iter().map(|r| r.1.len() / 2).max().unwrap();
|
||
assert_eq!(u32_at(b, 34) as usize, largest, "MaxRecord");
|
||
assert_eq!(recs.last().map(|r| (r.0, r.1.len())), Some((0, 6)), "META_EOF ends the file");
|
||
// Objects: the null pen and brush, the brush, the pen.
|
||
assert_eq!(u16_at(b, 32), 4);
|
||
// The curve is flattened into a polygon, its outline a polygon drawn with the null brush.
|
||
let poly = recs.iter().find(|r| r.0 == 0x0538).unwrap().1;
|
||
assert_eq!(u16_at(poly, 6), 1);
|
||
assert!(u16_at(poly, 8) > 16, "many points");
|
||
assert!(recs.iter().any(|r| r.0 == 0x0324));
|
||
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
|
||
}
|
||
|
||
#[test]
|
||
fn wmf_sizes_fit_sixteen_bits() {
|
||
assert_eq!(crate::wmf::units_per_inch(612.0, 792.0), 1440);
|
||
let inch = crate::wmf::units_per_inch(3000.0, 1000.0);
|
||
assert!(inch < 1440 && f64::from(inch) * 3000.0 / 72.0 <= 32767.0);
|
||
let d = doc_with(3000.0, 1000.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 3000.0, 1000.0)), red(), Paint::None, 0.0)]);
|
||
let b = wmf(&d).bytes;
|
||
assert_eq!(u16_at(&b, 14), inch);
|
||
assert!(u16_at(&b, 10) <= 32767);
|
||
}
|
||
|
||
#[test]
|
||
fn wmf_reports_what_it_leaves_out() {
|
||
let g = Paint::Gradient(Box::new(GradientPaint::new(Gradient::default())));
|
||
let mut clip = path(shapes::rectangle(Rect::new(0.0, 0.0, 20.0, 20.0)), Paint::None, Paint::None, 0.0);
|
||
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
||
*clipping = true;
|
||
}
|
||
let inside = path(shapes::rectangle(Rect::new(5.0, 5.0, 40.0, 40.0)), g, Paint::None, 0.0);
|
||
let group = Node::new(NodeId(0), NodeKind::Group { children: vec![Arc::new(clip), Arc::new(inside)], clip: true });
|
||
let mut d = doc_with(100.0, 100.0, vec![group]);
|
||
let img = image_node(&mut d, png(2, 2, [0, 0, 0, 10]), 2, 2, Affine::translate((50.0, 50.0)) * Affine::rotate(0.5));
|
||
add(&mut d, img);
|
||
let w = wmf(&d).warnings.join("\n");
|
||
for what in ["clipping masks", "gradients", "transparency", "rotate"] {
|
||
assert!(w.contains(what), "{what}: {w}");
|
||
}
|
||
// EMF keeps all of it.
|
||
let e = emf(&d).warnings;
|
||
assert!(e.is_empty(), "{e:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn hidden_and_offboard_art_is_left_out() {
|
||
let mut hidden = path(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), red(), Paint::None, 0.0);
|
||
hidden.visible = false;
|
||
let away = path(shapes::rectangle(Rect::new(500.0, 500.0, 510.0, 510.0)), red(), Paint::None, 0.0);
|
||
let d = doc_with(100.0, 100.0, vec![hidden, away]);
|
||
let k = kinds(&emf_records(&emf(&d).bytes));
|
||
assert!(!k.contains(&62) && !k.contains(&39));
|
||
assert!(export(&d, Rect::new(0.0, 0.0, 0.0, 10.0), Kind::Emf).is_err(), "an empty region");
|
||
}
|
||
|
||
#[test]
|
||
fn an_instance_whose_art_reaches_the_artboard_is_written() {
|
||
// The symbol's red rectangle, (100, 100) to (140, 140), drawn from (-50, -50): on the artboard,
|
||
// while the 20 × 20 square around the origin, moved to (-60, -60) to (-40, -40), is off it.
|
||
let inst = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((-50.0, -50.0)) });
|
||
let mut d = doc_with(100.0, 100.0, vec![inst]);
|
||
let art = path(shapes::rectangle(Rect::new(100.0, 100.0, 140.0, 140.0)), red(), Paint::None, 0.0);
|
||
d.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: Arc::new(art) });
|
||
let k = kinds(&emf_records(&emf(&d).bytes));
|
||
assert!(k.contains(&39), "a brush fills the art: {k:?}");
|
||
}
|
||
|
||
/// A pattern `name` repeating `art` every point.
|
||
fn point_tiles(name: &str, art: Node) -> PatternDef {
|
||
let mut def = PatternDef::new(name, vec![Arc::new(art)]);
|
||
def.tile = Rect::new(0.0, 0.0, 1.0, 1.0);
|
||
def
|
||
}
|
||
|
||
fn pattern(name: &str) -> Paint {
|
||
Paint::Pattern { pattern: name.into(), xf: Affine::IDENTITY }
|
||
}
|
||
|
||
#[test]
|
||
fn patterns_inside_pattern_tiles_share_one_tile_budget() {
|
||
// About 3,700 tiles of a 12 pt square, each filled with about 140 tiles of a dot: over half a
|
||
// million fills when every tile is written.
|
||
let square = path(shapes::rectangle(Rect::new(0.0, 0.0, 12.0, 12.0)), pattern("dots"), Paint::None, 0.0);
|
||
let mut d = doc_with(50.0, 50.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), pattern("squares"), Paint::None, 0.0)]);
|
||
d.patterns.push(point_tiles("dots", path(shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), red(), Paint::None, 0.0)));
|
||
d.patterns.push(point_tiles("squares", square));
|
||
let (ops, warnings) = crate::scene::Scene::new(&d, d.artboards[0].rect, true).run();
|
||
assert!(ops.len() < 300_000, "{} operations", ops.len());
|
||
assert!(warnings.iter().any(|w| w.contains("one colour")), "{warnings:?}");
|
||
|
||
// A tile filled with its own pattern multiplies at every level down to the nesting limit.
|
||
let square = path(shapes::rectangle(Rect::new(0.0, 0.0, 4.0, 4.0)), pattern("self"), Paint::None, 0.0);
|
||
let mut d = doc_with(10.0, 10.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), pattern("self"), Paint::None, 0.0)]);
|
||
d.patterns.push(point_tiles("self", square));
|
||
let (ops, _) = crate::scene::Scene::new(&d, d.artboards[0].rect, true).run();
|
||
assert!(ops.len() < 300_000, "{} operations", ops.len());
|
||
}
|