mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 15:51:56 +00:00
.ai import: page art moved to its artboard takes its pattern tiles along
Art taken from a `.ai`'s PDF part on a later page is moved to where the editing data puts that artboard. The move left pattern fills and strokes where they were, so their tiles no longer lined up with the art. This happened in three places: the page art that stands in for an unread object (#5050), the page's type given to text objects, and the art on each page when the file comes in as its PDF part with the artboard layout kept. Each move now transforms the pattern tiles too.
This commit is contained in:
@@ -15,6 +15,10 @@ pub(super) enum Page {
|
||||
Artboard,
|
||||
}
|
||||
|
||||
/// How art moved from a page to its place in the layers' document transforms: its pattern tiles
|
||||
/// move with it.
|
||||
pub(super) const PAGE_MOVE: Scaling = Scaling { strokes: false, effects: None, keep_type_strokes: false, keep_corners: false, patterns: true };
|
||||
|
||||
/// The page (`visible`, the file read as its page) and the place of its area in the layers'
|
||||
/// document.
|
||||
pub(super) struct Frame<'a> {
|
||||
@@ -177,7 +181,7 @@ pub(super) fn build_from(
|
||||
None => shift,
|
||||
};
|
||||
for n in object.iter_mut() {
|
||||
Arc::make_mut(n).transform(shift, false);
|
||||
Arc::make_mut(n).transform(shift, PAGE_MOVE);
|
||||
}
|
||||
}
|
||||
// An EPS saved as its artboard has the art outside it left out of its page.
|
||||
@@ -681,7 +685,7 @@ fn replace_unread(
|
||||
for &i in &selected {
|
||||
let mut copy = Node::clone(&art[i].node);
|
||||
if let Some(shift) = moves.get(page) {
|
||||
copy.transform(*shift, false);
|
||||
copy.transform(*shift, PAGE_MOVE);
|
||||
}
|
||||
found.push(Arc::new(reid_page_art(doc, ©)));
|
||||
claimed.insert(i, art[i].end);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::build::{Page, build, build_from};
|
||||
use super::build::{PAGE_MOVE, Page, build, build_from};
|
||||
use super::slots::LAYERS_UNREAD;
|
||||
use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect, Vec2, ai};
|
||||
|
||||
@@ -204,7 +204,7 @@ fn place_art(layer: &mut Node, pages: &[Rect], moves: &[Vec2]) {
|
||||
let fit = |p: &Rect| (p.intersect(r).area().max(0.0), -(p.center() - r.center()).hypot());
|
||||
let Some((i, _)) = pages.iter().map(fit).enumerate().max_by(|(_, a), (_, b)| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1))) else { continue };
|
||||
if let Some(v) = moves.get(i).filter(|v| **v != Vec2::ZERO) {
|
||||
Arc::make_mut(child).transform(Affine::translate(*v), false);
|
||||
Arc::make_mut(child).transform(Affine::translate(*v), PAGE_MOVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,6 +832,43 @@ fn unread_object_on_second_artboard_takes_second_page_art() {
|
||||
assert_eq!(bounds, vectorcraft_geom::Rect::new(50.0, 196.0, 54.0, 200.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_art_from_second_artboard_moves_its_pattern_tile() {
|
||||
let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\
|
||||
%_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_/Dictionary :\n%_0 -50 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 -150 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n";
|
||||
let dots = "%AI3_BeginPattern: (Dots)\n(Dots) 0 0 4 4\n%_0 0 0 1 k\n%_1 1 m\n%_3 1 L\n%_3 3 L\n%_1 3 L\n%_f\nE\n%AI3_EndPattern\n";
|
||||
let second = "u\n0 0 1 0 k\n50 -100 m\n54 -100 L\n54 -96 L\n50 -96 L\nf\nZZ\nU";
|
||||
let art = native(&(boards.to_string() + dots + &layer("Live", true, &square(10, 10)) + &layer("Mixed", true, second)));
|
||||
let (editing, _) = crate::ai_alone(&private(&native(&format!("{dots}{}", layer("Live", true, &square(10, 10)))))).unwrap();
|
||||
let source = editing.pattern("Dots").unwrap();
|
||||
let mut page = pdf_part(10.0, 86.0);
|
||||
let mut pattern = source.clone();
|
||||
pattern.art = source.art.iter().map(|node| std::sync::Arc::new(page.reid(node))).collect();
|
||||
page.patterns.push(pattern);
|
||||
let mut board = page.artboards[0].clone();
|
||||
(board.id, board.rect) = (2, vectorcraft_geom::Rect::new(120.0, 0.0, 220.0, 100.0));
|
||||
page.artboards.push(board);
|
||||
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(170.0, 46.0, 174.0, 50.0));
|
||||
let paint = vectorcraft_color::Paint::Pattern { pattern: "Dots".into(), xf: vectorcraft_geom::Affine::IDENTITY };
|
||||
let appearance = vectorcraft_doc::Appearance {
|
||||
items: vec![vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint))],
|
||||
..Default::default()
|
||||
};
|
||||
let node = Node::path(page.alloc_id(), path, appearance);
|
||||
let layer = page.layers[0].id;
|
||||
page.insert(Some(layer), 1, node).unwrap();
|
||||
let (doc, notes, read) = crate::layered_ai(&private(&art), page, vec![], false, false, 1.0);
|
||||
assert!(read, "{notes:?}");
|
||||
let replacement = &doc.layers[1].children().unwrap()[0].children().unwrap()[0];
|
||||
assert_eq!(replacement.visual_bounds().unwrap(), vectorcraft_geom::Rect::new(50.0, 196.0, 54.0, 200.0));
|
||||
let Some(vectorcraft_color::Paint::Pattern { xf, .. }) = replacement.appearance.fill().map(|f| &f.paint) else {
|
||||
panic!("the page art's pattern fill is kept: {:?}", replacement.appearance);
|
||||
};
|
||||
assert_eq!(*xf, vectorcraft_geom::Affine::translate(vectorcraft_geom::Vec2::new(-120.0, 150.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_operator_between_objects_falls_back() {
|
||||
let body = format!("{}\nZZ\n{}", square(50, 10), square(70, 10));
|
||||
@@ -1443,6 +1480,38 @@ fn an_ai_file_read_as_its_pdf_part_keeps_its_artboard_layout() {
|
||||
assert!((moved[1] - vectorcraft_geom::Point::new(100.0, 1600.0)).hypot() < 1e-6, "the second's art goes with its page: {moved:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_moved_with_its_page_moves_its_pattern_tile() {
|
||||
// Two 100-point artboards, the second 50 below the first; an operator this doesn't know, so
|
||||
// the layers aren't read.
|
||||
let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\
|
||||
%_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_/Dictionary :\n%_0 -50 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 -150 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n";
|
||||
let art = native(&(boards.to_string() + &layer("Art", true, &(square(10, 10) + "\n1 Zz"))));
|
||||
// The PDF part: the second page 20 points right of the first, with a pattern-filled square.
|
||||
let mut pdf = pdf_part(10.0, 10.0);
|
||||
let mut page = pdf.artboards[0].clone();
|
||||
(page.id, page.rect) = (2, vectorcraft_geom::Rect::new(120.0, 0.0, 220.0, 100.0));
|
||||
pdf.artboards.push(page);
|
||||
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(130.0, 10.0, 134.0, 14.0));
|
||||
let paint = vectorcraft_color::Paint::Pattern { pattern: "Dots".into(), xf: vectorcraft_geom::Affine::IDENTITY };
|
||||
let appearance = vectorcraft_doc::Appearance {
|
||||
items: vec![vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint))],
|
||||
..Default::default()
|
||||
};
|
||||
let (id, layer_id) = (pdf.alloc_id(), pdf.layers[0].id);
|
||||
pdf.insert(Some(layer_id), 1, Node::path(id, path, appearance)).unwrap();
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), pdf, vec![], false, false, 1.0);
|
||||
assert!(!read, "{w:?}");
|
||||
let square = &d.layers[0].children().unwrap()[1];
|
||||
assert_eq!(square.visual_bounds().unwrap().origin(), vectorcraft_geom::Point::new(10.0, 160.0));
|
||||
let Some(vectorcraft_color::Paint::Pattern { xf, .. }) = square.appearance.fill().map(|f| &f.paint) else {
|
||||
panic!("the square keeps its pattern fill: {:?}", square.appearance);
|
||||
};
|
||||
assert_eq!(*xf, vectorcraft_geom::Affine::translate(vectorcraft_geom::Vec2::new(-120.0, 150.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gray_is_a_grayscale_colour() {
|
||||
// `g` is a gray: 0.25 is 75 % of the black ink, as the page's own CMYK black says; the colour
|
||||
@@ -2126,6 +2195,41 @@ fn type_on_a_later_artboard_of_an_ai_file_goes_to_its_text_object() {
|
||||
assert!(!w.iter().any(|w| w.contains("with another's") || w.contains("doesn't draw")), "{w:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_type_on_a_later_artboard_moves_its_pattern_tile() {
|
||||
// As above, with a frame this doesn't read, so the text object takes the page's type, which
|
||||
// has a pattern fill.
|
||||
let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\
|
||||
%_/Dictionary :\n%_0 100 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_/Dictionary :\n%_0 -50 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_100 -150 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n";
|
||||
let doc = last_frame_unread(&text_document(&[("Hi there\r", 0, (8200.0, 8341.5), (0.0, 0.0), 0.0)]));
|
||||
let art = native_with_text(&(boards.to_string() + &layer("Art", true, &(square(10, 10) + &text_object(0)))), &doc);
|
||||
let mut pdf = pdf_part(10.0, 86.0);
|
||||
let mut page = pdf.artboards[0].clone();
|
||||
(page.id, page.rect) = (2, vectorcraft_geom::Rect::new(120.0, 0.0, 220.0, 100.0));
|
||||
pdf.artboards.push(page);
|
||||
let style = vectorcraft_doc::CharStyle { size: 14.0, ..Default::default() };
|
||||
let text = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(178.5, 50.0), "Hi there", style);
|
||||
let (id, layer_id) = (pdf.alloc_id(), pdf.layers[0].id);
|
||||
let mut node = Node::new(id, NodeKind::Text(Box::new(text)));
|
||||
let paint = vectorcraft_color::Paint::Pattern { pattern: "Dots".into(), xf: vectorcraft_geom::Affine::IDENTITY };
|
||||
node.appearance = vectorcraft_doc::Appearance {
|
||||
items: vec![vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(paint))],
|
||||
..Default::default()
|
||||
}
|
||||
.into();
|
||||
pdf.insert(Some(layer_id), 1, node).unwrap();
|
||||
let (d, w, read) = crate::layered_ai(&private(&art), pdf, vec![], false, false, 1.0);
|
||||
assert!(read, "{w:?}");
|
||||
let texts: Vec<&Node> = d.layers[0].children().unwrap().iter().filter(|n| matches!(n.kind, NodeKind::Text(_))).map(|n| &**n).collect();
|
||||
assert_eq!(texts.len(), 1, "{w:?}");
|
||||
let Some(vectorcraft_color::Paint::Pattern { xf, .. }) = texts[0].appearance.fill().map(|f| &f.paint) else {
|
||||
panic!("the page's type keeps its pattern fill: {:?}", texts[0].appearance);
|
||||
};
|
||||
assert_eq!(*xf, vectorcraft_geom::Affine::translate(vectorcraft_geom::Vec2::new(-120.0, 150.0)));
|
||||
}
|
||||
|
||||
/// A global process colour (`Xk`) or a pattern fill on a sublayer no longer sends a `.ai` to its
|
||||
/// PDF part, which has only the top-level layers (#1025).
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user