Merge pull request #2016 from EricKuck/fix/ai-pattern-phase

AI import: a pattern fill tiles from its tile's lower left, wherever the definition drew the tile
This commit is contained in:
BFlat
2026-10-16 02:38:08 +07:00
committed by GitHub
2 changed files with 23 additions and 6 deletions
+8 -6
View File
@@ -400,8 +400,8 @@ struct Reader<'a> {
large_canvas: Option<f64>,
/// This reader reads a pattern's definition (see [`Reader::define_pattern`]).
pattern: Option<PatternHead>,
/// The tile's top left (y down) of each pattern read, by name.
pattern_tiles: HashMap<String, Point>,
/// The tile (y down) of each pattern read, by name.
pattern_tiles: HashMap<String, Rect>,
/// The legacy text object being read (`To` … `TO`), and the modal type attributes.
text: Option<legacy_text::LegacyText>,
type_state: legacy_text::TypeState,
@@ -739,7 +739,7 @@ impl<'a> Reader<'a> {
let art: Vec<Arc<Node>> = frame.children.iter().map(|n| Arc::new(self.doc.reid(n))).collect();
let mut def = PatternDef::new(&head.name, art);
def.tile = FLIP.transform_rect_bbox(tile);
if self.pattern_tiles.insert(def.name.clone(), def.tile.origin()).is_some() {
if self.pattern_tiles.insert(def.name.clone(), def.tile).is_some() {
self.doc.patterns.retain(|p| p.name != def.name);
}
self.doc.patterns.push(def);
@@ -1411,13 +1411,15 @@ impl<'a> Reader<'a> {
return;
}
let name = vals.iter().find_map(|v| if let V::Str(_) = v { v.text() } else { None });
let Some(origin) = name.as_deref().and_then(|n| self.pattern_tiles.get(n)).map(|p| p.to_vec2()) else {
let Some(tile) = name.as_deref().and_then(|n| self.pattern_tiles.get(n)).copied() else {
self.unreadable("pattern fills");
return;
};
let matrix = vals.iter().rev().find_map(|v| if let V::Arr(a) = v { paint::affine(&Self::nums(a)) } else { None });
// The tile's art (y down from the tile's top left) → the pattern's space → art space.
let xf = self.space() * matrix.unwrap_or(Affine::IDENTITY) * FLIP * Affine::translate(origin);
// The tile's art (y down from the tile's top left) → the pattern's space → art space. The
// tile's lower left is the pattern's origin wherever the definition drew it, as the file's
// PDF part tiles it.
let xf = self.space() * matrix.unwrap_or(Affine::IDENTITY) * FLIP * Affine::translate((0.0, -tile.height()));
if xf.determinant().abs() < 1e-12 || !xf.as_coeffs().iter().all(|v| v.is_finite()) {
self.unreadable("pattern fills");
return;
+15
View File
@@ -202,6 +202,21 @@ fn patterns_are_pattern_swatches_and_fill_with_them() {
assert!(s.warnings.is_empty(), "{:?}", s.warnings);
}
/// Where a definition draws its tile doesn't move the fill: the tile's lower left is the pattern's
/// origin, so the same dots drawn at 30..38 × 40..48 tile as they do at 0..8 × 0..8.
#[test]
fn a_pattern_tiles_from_its_lower_left_wherever_its_definition_drew_it() {
let moved = DOTS
.replace("(Dots) 0 0 8 8", "(Dots) 30 40 38 48")
.replace("%_0 8 m\n%_8 8 L\n%_8 0 L\n%_0 0 L\n%_0 8 L", "%_30 48 m\n%_38 48 L\n%_38 40 L\n%_30 40 L\n%_30 48 L")
.replace("%_4 4 m\n%_4 4 L", "%_34 44 m\n%_34 44 L");
let body = format!("{moved}{}", layer(&format!("(Dots) 0 0 1 1 0 0 0 0 0 [1 0 0 1 0 0] p\n{}f\n", rect_path(10.0, 20.0, 70.0, 80.0))));
let s = read_ok(&body);
assert_eq!(s.doc.pattern("Dots").unwrap().tile, Rect::new(30.0, -48.0, 38.0, -40.0));
let Paint::Pattern { xf, .. } = fill(&art(&s.doc)[0]) else { panic!() };
assert_eq!(xf * Point::ZERO, Point::new(0.0, 92.0));
}
/// A pattern whose definition it can't read whole isn't kept: a fill with it is something the file
/// has that isn't read, where it shows. Its definition costs nothing else.
#[test]