//! Pattern swatches (Object → Pattern) and live Repeats (Object → Repeat): data and pure math. //! //! **Patterns.** A [`PatternDef`] is a named tile: art plus a tile rectangle and a tile type //! (grid, brick by row/column, hex by column/row). Pattern space has the tile's top-left at the //! origin; instance `(i, j)` is the art translated by `-tile.origin + tile_offset(i, j)`. A //! [`Paint::Pattern`] maps pattern space into the document with its own `xf` (identity = tiles //! anchored at the document origin, like Illustrator's ruler origin). Every tiling is periodic //! on a rectangular *super-tile* ([`PatternDef::period`]), which is what renderers rasterize and //! what SVG `` elements repeat. //! //! **Repeats.** A [`RepeatSpec`] holds source art and a radial / grid / mirror arrangement; the //! instances are the source under [`repeat_transforms`] (evaluated on demand like blends). use std::sync::Arc; use serde::{Deserialize, Serialize}; use vectorcraft_color::Paint; use vectorcraft_geom::{Affine, Point, Rect, Vec2}; use crate::appearance::AppearanceItem; use crate::node::{Node, NodeId, NodeKind}; fn half() -> f64 { 0.5 } fn five() -> u32 { 5 } fn seventy() -> f32 { 70.0 } fn yes() -> bool { true } fn three() -> u32 { 3 } fn full_turn() -> f64 { 360.0 } // ===================================================================================== // Patterns // ===================================================================================== /// Pattern Options → Tile Type. #[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum TileType { #[default] Grid, /// Rows shifted horizontally by `offset` × tile width (0.5 = half brick). BrickByRow { #[serde(default = "half")] offset: f64, }, /// Columns shifted vertically by `offset` × tile height. BrickByColumn { #[serde(default = "half")] offset: f64, }, /// Hexagonal tiles in columns: columns ¾ tile width apart, odd columns shifted half a tile down. HexByColumn, /// Hexagonal tiles in rows: rows ¾ tile height apart, odd rows shifted half a tile right. HexByRow, } impl TileType { pub fn id(&self) -> &'static str { match self { TileType::Grid => "grid", TileType::BrickByRow { .. } => "brickByRow", TileType::BrickByColumn { .. } => "brickByColumn", TileType::HexByColumn => "hexByColumn", TileType::HexByRow => "hexByRow", } } pub fn label(&self) -> &'static str { match self { TileType::Grid => "Grid", TileType::BrickByRow { .. } => "Brick by Row", TileType::BrickByColumn { .. } => "Brick by Column", TileType::HexByColumn => "Hex by Column", TileType::HexByRow => "Hex by Row", } } /// Parse an id (`grid`, `brickByRow`, `brick-row`, …) with an optional brick offset. pub fn parse(s: &str, offset: Option) -> Option { let o = offset.unwrap_or(0.5); Some(match s.to_ascii_lowercase().replace(['-', '_', ' '], "").as_str() { "grid" => TileType::Grid, "brickbyrow" | "brickrow" | "brick" => TileType::BrickByRow { offset: o }, "brickbycolumn" | "brickcolumn" | "brickcol" => TileType::BrickByColumn { offset: o }, "hexbycolumn" | "hexcolumn" | "hexcol" | "hex" => TileType::HexByColumn, "hexbyrow" | "hexrow" => TileType::HexByRow, _ => return None, }) } pub const IDS: [&'static str; 5] = ["grid", "brickByRow", "brickByColumn", "hexByColumn", "hexByRow"]; /// Column / row steps for a `w`×`h` tile (the hex types pack columns/rows at ¾). pub fn steps(&self, w: f64, h: f64) -> (f64, f64) { match self { TileType::HexByColumn => (w * 0.75, h), TileType::HexByRow => (w, h * 0.75), _ => (w, h), } } } /// Position of instance (column `i`, row `j`) for a `w`×`h` tile. `(0, 0)` is at the origin. pub fn tile_offset(tt: TileType, w: f64, h: f64, i: i64, j: i64) -> Vec2 { let (i_f, j_f) = (i as f64, j as f64); let frac = |v: f64| v - v.floor(); match tt { TileType::Grid => Vec2::new(i_f * w, j_f * h), TileType::BrickByRow { offset } => Vec2::new(i_f * w + frac(j_f * offset) * w, j_f * h), TileType::BrickByColumn { offset } => Vec2::new(i_f * w, j_f * h + frac(i_f * offset) * h), TileType::HexByColumn => Vec2::new(i_f * w * 0.75, j_f * h + if i.rem_euclid(2) == 1 { h / 2.0 } else { 0.0 }), TileType::HexByRow => Vec2::new(i_f * w + if j.rem_euclid(2) == 1 { w / 2.0 } else { 0.0 }, j_f * h * 0.75), } } /// Smallest `q` in 1..=24 with `q·offset` (almost) an integer (24 when none is). fn brick_repeat(offset: f64) -> f64 { for q in 1..=24 { let v = q as f64 * offset; if (v - v.round()).abs() < 1e-6 { return q as f64; } } 24.0 } /// Size of the rectangular super-tile a tiling repeats on. pub fn tile_period(tt: TileType, w: f64, h: f64) -> (f64, f64) { match tt { TileType::Grid => (w, h), TileType::BrickByRow { offset } => (w, h * brick_repeat(offset)), TileType::BrickByColumn { offset } => (w * brick_repeat(offset), h), TileType::HexByColumn => (w * 1.5, h), TileType::HexByRow => (w, h * 1.5), } } /// Pattern Options → Overlap. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Overlap { /// Right in front (default: left in front). #[serde(default)] pub right_in_front: bool, /// Bottom in front (default: top in front). #[serde(default)] pub bottom_in_front: bool, } /// A pattern swatch definition. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PatternDef { pub name: String, /// Tile rectangle in the art's coordinates. pub tile: Rect, #[serde(default)] pub tile_type: TileType, pub art: Vec>, #[serde(default)] pub overlap: Overlap, /// Size Tile to Art: the tile is the art bounds grown by the spacing. #[serde(default)] pub size_tile_to_art: bool, #[serde(default)] pub h_spacing: f64, #[serde(default)] pub v_spacing: f64, /// Pattern editing preview: copies per side (3, 5, 7, 9). #[serde(default = "five")] pub copies: u32, /// Dim Copies to (percent opacity of the preview copies). #[serde(default = "seventy")] pub dim_copies: f32, #[serde(default = "yes")] pub show_tile_edge: bool, /// Show Swatch Bounds: pattern editing mode outlines the part of the tiling the swatch repeats /// (the tile, or the period of a brick or hex tiling), dashed. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub show_swatch_bounds: bool, } impl PatternDef { /// A pattern of `art` whose tile is the art's bounds. pub fn new(name: &str, art: Vec>) -> Self { let tile = crate::live::nodes_bounds(&art).unwrap_or(Rect::new(0.0, 0.0, 10.0, 10.0)); Self { name: name.to_string(), tile, tile_type: TileType::Grid, art, overlap: Overlap::default(), size_tile_to_art: false, h_spacing: 0.0, v_spacing: 0.0, copies: 5, dim_copies: 70.0, show_tile_edge: true, show_swatch_bounds: false, } } pub fn width(&self) -> f64 { self.tile.width().max(1e-3) } pub fn height(&self) -> f64 { self.tile.height().max(1e-3) } /// Visual bounds of the art (stroke included). pub fn art_bounds(&self) -> Option { self.art.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.visual_bounds())) } /// Re-derive the tile from the art (Size Tile to Art). pub fn fit_tile_to_art(&mut self) { if let Some(b) = crate::live::nodes_bounds(&self.art) { self.tile = Rect::new(b.x0 - self.h_spacing / 2.0, b.y0 - self.v_spacing / 2.0, b.x1 + self.h_spacing / 2.0, b.y1 + self.v_spacing / 2.0); } } /// Offset of instance (column `i`, row `j`) in pattern space. pub fn offset(&self, i: i64, j: i64) -> Vec2 { tile_offset(self.tile_type, self.width(), self.height(), i, j) } /// The rectangular period of the tiling (the super-tile). pub fn period(&self) -> (f64, f64) { tile_period(self.tile_type, self.width(), self.height()) } /// The swatch bounds (Show Swatch Bounds): the period of the tiling from the tile's origin. pub fn swatch_bounds(&self) -> Rect { let (w, h) = self.period(); Rect::from_origin_size(self.tile.origin(), (w, h)) } /// Maps the art's coordinates into pattern space for instance offset `o`. pub fn instance_xf(&self, o: Vec2) -> Affine { Affine::translate(o - self.tile.origin().to_vec2()) } /// Offsets (pattern space) of every instance whose art touches `region` (pattern space), in /// paint order: the instances "in front" per [`Overlap`] come last. pub fn offsets_covering(&self, region: Rect) -> Vec { let (w, h) = (self.width(), self.height()); // Art extent relative to the tile's origin (at least the tile itself). let ab = self.art_bounds().unwrap_or(self.tile).union(self.tile) - self.tile.origin().to_vec2(); let (sx, sy) = self.tile_type.steps(w, h); // Shifts of brick/hex rows or columns are < one tile. let (i0, i1) = (((region.x0 - ab.x1 - w) / sx).floor() as i64 - 1, ((region.x1 - ab.x0 + w) / sx).ceil() as i64 + 1); let (j0, j1) = (((region.y0 - ab.y1 - h) / sy).floor() as i64 - 1, ((region.y1 - ab.y0 + h) / sy).ceil() as i64 + 1); if (i1 - i0).saturating_mul(j1 - j0) > 250_000 { return vec![]; } let mut out = vec![]; let rows: Vec = if self.overlap.bottom_in_front { (j0..=j1).collect() } else { (j0..=j1).rev().collect() }; for &j in &rows { let cols: Vec = if self.overlap.right_in_front { (i0..=i1).collect() } else { (i0..=i1).rev().collect() }; for i in cols { let o = self.offset(i, j); let r = ab + o; if r.x0 < region.x1 && r.x1 > region.x0 && r.y0 < region.y1 && r.y1 > region.y0 { out.push(o); } } } out } /// Offsets of the pattern-editing preview copies (`copies`×`copies` around the tile, the /// centre `(0, 0)` excluded). pub fn preview_offsets(&self) -> Vec { let k = (self.copies.clamp(1, 15) / 2) as i64; let mut out = vec![]; for j in -k..=k { for i in -k..=k { if (i, j) != (0, 0) { out.push(self.offset(i, j)); } } } out } /// Instances covering `region` (document space) as plain nodes for a pattern painted with /// `paint_xf` (pattern space → document), fresh-id-less (id 0). Used by exporters and Expand. pub fn instances_in(&self, paint_xf: Affine, region: Rect) -> Vec { let inv = paint_xf.inverse(); let pr = inv.transform_rect_bbox(region); let mut out = vec![]; for o in self.offsets_covering(pr) { let m = paint_xf * self.instance_xf(o); for a in &self.art { let mut n = (**a).clone(); n.transform(m, false); zero_ids(&mut n); out.push(n); } } out } } fn zero_ids(n: &mut Node) { n.id = NodeId(0); if let Some(ch) = n.children_mut() { for c in ch.iter_mut() { zero_ids(Arc::make_mut(c)); } } } /// Pattern editing mode state (lives in the document so undo and the renderer see it). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PatternEdit { /// Pattern being edited. pub pattern: String, /// The temporary layer holding the tile art while editing. pub layer: NodeId, /// The definition before editing (restored by Cancel); `None` when Make created it. #[serde(default, skip_serializing_if = "Option::is_none")] pub original: Option, } /// Name of the temporary pattern-editing layer. pub const PATTERN_EDIT_LAYER: &str = "Pattern Editing Mode"; /// A pattern paint (identity placement). pub fn pattern_paint(name: &str) -> Paint { Paint::Pattern { pattern: name.to_string(), xf: Affine::IDENTITY } } /// Does `n` (not its children) paint any fill/stroke with pattern `name` (any pattern for `None`)? pub fn uses_pattern(n: &Node, name: Option<&str>) -> bool { n.appearance.items.iter().any(|it| { let p = match it { AppearanceItem::Fill(f) => &f.paint, AppearanceItem::Stroke(s) => &s.paint, }; matches!(p, Paint::Pattern { pattern, .. } if name.is_none_or(|nm| nm == pattern)) }) } /// Apply `m` to the pattern placement of every pattern paint of `n` (Transform Patterns). pub fn transform_pattern_paints(n: &mut Node, m: Affine) { for it in n.appearance.items.iter_mut() { let p = match it { AppearanceItem::Fill(f) => &mut f.paint, AppearanceItem::Stroke(s) => &mut s.paint, }; if let Paint::Pattern { xf, .. } = p { *xf = m * *xf; } } } // ===================================================================================== // Repeat // ===================================================================================== /// The arrangement of a Repeat object. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum RepeatKind { /// Copies rotated around `center`; the source's centre is kept `radius` from it. #[serde(rename_all = "camelCase")] Radial { instances: u32, radius: f64, center: Point, #[serde(default)] reverse_overlap: bool, #[serde(default)] start_angle: f64, #[serde(default = "full_turn")] end_angle: f64, }, /// Copies on a grid (tile = source bounds + spacing), optionally brick/hex offset and with /// alternate rows/columns flipped. #[serde(rename_all = "camelCase")] Grid { h_spacing: f64, v_spacing: f64, #[serde(default = "three")] rows: u32, #[serde(default = "three")] cols: u32, #[serde(default)] grid_type: TileType, /// Flip alternate rows vertically. #[serde(default)] flip_rows: bool, /// Flip alternate columns horizontally. #[serde(default)] flip_cols: bool, }, /// The source plus its reflection across the axis through `center` at `angle` degrees /// (90 = vertical axis). Mirror { angle: f64, center: Point }, } impl RepeatKind { pub fn label(&self) -> &'static str { match self { RepeatKind::Radial { .. } => "Radial Repeat", RepeatKind::Grid { .. } => "Grid Repeat", RepeatKind::Mirror { .. } => "Mirror Repeat", } } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RepeatSpec { pub kind: RepeatKind, pub source: Vec>, } /// Reflection across the line through `c` with direction angle `deg`. pub fn reflect_about(c: Point, deg: f64) -> Affine { let t = (2.0 * deg).to_radians(); let (s2, c2) = t.sin_cos(); Affine::translate(c.to_vec2()) * Affine::new([c2, s2, s2, -c2, 0.0, 0.0]) * Affine::translate(-c.to_vec2()) } /// Instance transforms of a repeat (source space → document), in paint order. pub fn repeat_transforms(kind: &RepeatKind, source_bounds: Option) -> Vec { let b = source_bounds.unwrap_or(Rect::new(0.0, 0.0, 1.0, 1.0)); match kind { RepeatKind::Radial { instances, radius, center, reverse_overlap, start_angle, end_angle } => { let n = (*instances).clamp(1, 1000); let c0 = b.center(); let mut d = c0 - *center; if d.hypot() < 1e-9 { d = Vec2::new(0.0, -1.0); } let pre = Affine::translate(*center + d.normalize() * radius.max(0.0) - c0); let sweep = end_angle - start_angle; let full = (sweep.abs() - 360.0).abs() < 1e-9; let step = if full || n == 1 { sweep / n as f64 } else { sweep / (n - 1) as f64 }; let mut v: Vec = (0..n) .map(|k| { let a = (start_angle + step * k as f64).to_radians(); Affine::translate(center.to_vec2()) * Affine::rotate(a) * Affine::translate(-center.to_vec2()) * pre }) .collect(); if *reverse_overlap { v.reverse(); } v } RepeatKind::Grid { h_spacing, v_spacing, rows, cols, grid_type, flip_rows, flip_cols } => { let (w, h) = ((b.width() + h_spacing).max(1e-3), (b.height() + v_spacing).max(1e-3)); let mut v = vec![]; for j in 0..(*rows).clamp(1, 500) as i64 { for i in 0..(*cols).clamp(1, 500) as i64 { let o = tile_offset(*grid_type, w, h, i, j); let c = b.center(); let mut flip = Affine::IDENTITY; if *flip_cols && i % 2 == 1 { flip = reflect_about(c, 90.0) * flip; } if *flip_rows && j % 2 == 1 { flip = reflect_about(c, 0.0) * flip; } v.push(Affine::translate(o) * flip); } } v } RepeatKind::Mirror { angle, center } => vec![Affine::IDENTITY, reflect_about(*center, *angle)], } } impl RepeatSpec { pub fn source_bounds(&self) -> Option { crate::live::nodes_bounds(&self.source) } pub fn transforms(&self) -> Vec { repeat_transforms(&self.kind, self.source_bounds()) } /// Evaluated instances: one group of the (transformed) source per instance (ids 0). pub fn expand(&self) -> Vec { self.transforms() .into_iter() .map(|m| { let children = self .source .iter() .map(|s| { let mut n = (**s).clone(); n.transform(m, false); zero_ids(&mut n); Arc::new(n) }) .collect(); Node::group(NodeId(0), children) }) .collect() } /// Geometric bounds of all instances. pub fn bounds(&self) -> Option { let b = self.source_bounds()?; self.transforms().into_iter().fold(None, |acc, m| vectorcraft_geom::union_opt(acc, Some(m.transform_rect_bbox(b)))) } /// Apply a document transform (source + arrangement parameters). pub fn transform(&mut self, a: Affine, scaling: impl Into) { let scaling = scaling.into(); for c in self.source.iter_mut() { Arc::make_mut(c).transform(a, scaling); } let s = a.determinant().abs().sqrt(); match &mut self.kind { RepeatKind::Radial { radius, center, .. } => { *center = a * *center; *radius *= s; } RepeatKind::Grid { h_spacing, v_spacing, .. } => { *h_spacing *= s; *v_spacing *= s; } RepeatKind::Mirror { angle, center } => { *center = a * *center; let d = a * Point::new(angle.to_radians().cos(), angle.to_radians().sin()) - a * Point::ZERO; *angle = d.y.atan2(d.x).to_degrees(); } } } /// Default radial repeat around the source (centre below it, radius = its height × 1.2). pub fn radial(source: Vec>, instances: u32, radius: Option) -> Self { let b = crate::live::nodes_bounds(&source).unwrap_or(Rect::new(0.0, 0.0, 10.0, 10.0)); let r = radius.unwrap_or((b.height().max(b.width()) * 1.2).max(10.0)); let center = b.center() + Vec2::new(0.0, r); Self { kind: RepeatKind::Radial { instances, radius: r, center, reverse_overlap: false, start_angle: 0.0, end_angle: 360.0 }, source } } pub fn grid(source: Vec>, h_spacing: f64, v_spacing: f64) -> Self { Self { kind: RepeatKind::Grid { h_spacing, v_spacing, rows: 3, cols: 3, grid_type: TileType::Grid, flip_rows: false, flip_cols: false }, source, } } /// Mirror across an axis at `angle` through a point `offset` beyond the source's right edge /// (vertical axis) or below it (other angles pass through the right-edge midpoint). pub fn mirror(source: Vec>, angle: f64, offset: f64) -> Self { let b = crate::live::nodes_bounds(&source).unwrap_or(Rect::new(0.0, 0.0, 10.0, 10.0)); let center = Point::new(b.x1 + offset, b.center().y); Self { kind: RepeatKind::Mirror { angle, center }, source } } } /// Is this a Repeat object? pub fn is_repeat(n: &Node) -> bool { matches!(n.kind, NodeKind::Repeat(_)) } #[cfg(test)] mod tests { use super::*; use crate::Appearance; use vectorcraft_geom::shapes; fn sq(x: f64, y: f64, s: f64) -> Arc { Arc::new(Node::path(NodeId(1), shapes::rectangle(Rect::new(x, y, x + s, y + s)), Appearance::default())) } fn close(a: Vec2, b: Vec2) -> bool { (a - b).hypot() < 1e-9 } #[test] fn grid_offsets() { assert!(close(tile_offset(TileType::Grid, 10.0, 20.0, 2, 3), Vec2::new(20.0, 60.0))); assert!(close(tile_offset(TileType::Grid, 10.0, 20.0, -1, -1), Vec2::new(-10.0, -20.0))); assert_eq!(tile_period(TileType::Grid, 10.0, 20.0), (10.0, 20.0)); } #[test] fn brick_by_row_offsets() { let t = TileType::BrickByRow { offset: 0.5 }; assert!(close(tile_offset(t, 10.0, 20.0, 0, 1), Vec2::new(5.0, 20.0))); assert!(close(tile_offset(t, 10.0, 20.0, 0, 2), Vec2::new(0.0, 40.0))); assert!(close(tile_offset(t, 10.0, 20.0, 1, -1), Vec2::new(15.0, -20.0))); assert_eq!(tile_period(t, 10.0, 20.0), (10.0, 40.0)); let t3 = TileType::BrickByRow { offset: 1.0 / 3.0 }; assert_eq!(tile_period(t3, 10.0, 20.0), (10.0, 60.0)); } #[test] fn brick_by_column_offsets() { let t = TileType::BrickByColumn { offset: 0.25 }; assert!(close(tile_offset(t, 10.0, 20.0, 1, 0), Vec2::new(10.0, 5.0))); assert!(close(tile_offset(t, 10.0, 20.0, 3, 1), Vec2::new(30.0, 35.0))); assert_eq!(tile_period(t, 10.0, 20.0), (40.0, 20.0)); } #[test] fn hex_offsets() { let c = TileType::HexByColumn; assert!(close(tile_offset(c, 10.0, 20.0, 1, 0), Vec2::new(7.5, 10.0))); assert!(close(tile_offset(c, 10.0, 20.0, 2, 1), Vec2::new(15.0, 20.0))); assert!(close(tile_offset(c, 10.0, 20.0, -1, 0), Vec2::new(-7.5, 10.0))); assert_eq!(tile_period(c, 10.0, 20.0), (15.0, 20.0)); let r = TileType::HexByRow; assert!(close(tile_offset(r, 10.0, 20.0, 0, 1), Vec2::new(5.0, 15.0))); assert!(close(tile_offset(r, 10.0, 20.0, 1, 2), Vec2::new(10.0, 30.0))); assert_eq!(tile_period(r, 10.0, 20.0), (10.0, 30.0)); } #[test] fn tilings_are_periodic_on_their_period() { // Every instance shifted by the period is again an instance. for tt in [ TileType::Grid, TileType::BrickByRow { offset: 0.5 }, TileType::BrickByColumn { offset: 0.25 }, TileType::HexByColumn, TileType::HexByRow, ] { let (w, h) = (10.0, 20.0); let (pw, ph) = tile_period(tt, w, h); let all: Vec = (-8..8).flat_map(|j| (-8..8).map(move |i| tile_offset(tt, w, h, i, j))).collect(); for o in all.iter().filter(|o| o.x.abs() < 30.0 && o.y.abs() < 30.0) { for d in [Vec2::new(pw, 0.0), Vec2::new(0.0, ph)] { assert!(all.iter().any(|p| close(*p, *o + d)), "{tt:?} {o:?}+{d:?}"); } } } } #[test] fn tile_type_parse() { assert_eq!(TileType::parse("brickByRow", Some(0.25)), Some(TileType::BrickByRow { offset: 0.25 })); assert_eq!(TileType::parse("hex-by-row", None), Some(TileType::HexByRow)); assert_eq!(TileType::parse("nope", None), None); for id in TileType::IDS { assert_eq!(TileType::parse(id, None).unwrap().id(), id); } } #[test] fn covering_offsets_and_overlap_order() { let def = PatternDef::new("p", vec![sq(0.0, 0.0, 10.0)]); let offs = def.offsets_covering(Rect::new(0.0, 0.0, 30.0, 20.0)); // 3×2 tiles fully inside, plus neighbours touching the edges are excluded (strict overlap). assert_eq!(offs.len(), 6, "{offs:?}"); // Left/top in front: the top-left tile is drawn last. assert!(close(*offs.last().unwrap(), Vec2::ZERO)); let mut d2 = def.clone(); d2.overlap = Overlap { right_in_front: true, bottom_in_front: true }; assert!(close(*d2.offsets_covering(Rect::new(0.0, 0.0, 30.0, 20.0)).last().unwrap(), Vec2::new(20.0, 10.0))); } #[test] fn instances_follow_paint_xf() { let def = PatternDef::new("p", vec![sq(100.0, 100.0, 10.0)]); let n = def.instances_in(Affine::translate((3.0, 0.0)), Rect::new(0.0, 0.0, 20.0, 10.0)); let xs: Vec = n.iter().filter_map(|n| n.geometric_bounds()).map(|b| b.x0).collect(); assert!(xs.contains(&3.0) && xs.contains(&-7.0) && xs.contains(&13.0), "{xs:?}"); assert!(n.iter().all(|n| n.id == NodeId(0))); } #[test] fn preview_offsets_count() { let mut def = PatternDef::new("p", vec![sq(0.0, 0.0, 10.0)]); assert_eq!(def.preview_offsets().len(), 24); def.copies = 3; assert_eq!(def.preview_offsets().len(), 8); } #[test] fn radial_transforms() { let r = RepeatSpec::radial(vec![sq(-5.0, -105.0, 10.0)], 4, Some(100.0)); let RepeatKind::Radial { center, .. } = r.kind else { panic!() }; assert!((center - Point::new(0.0, -0.0)).hypot() < 1e-9, "{center:?}"); let cs: Vec = r.transforms().iter().map(|m| *m * Point::new(0.0, -100.0)).collect(); assert!((cs[0] - Point::new(0.0, -100.0)).hypot() < 1e-9); assert!((cs[1] - Point::new(100.0, 0.0)).hypot() < 1e-9, "{cs:?}"); assert!((cs[2] - Point::new(0.0, 100.0)).hypot() < 1e-9); assert_eq!(r.expand().len(), 4); } #[test] fn radial_radius_is_authoritative() { let mut r = RepeatSpec::radial(vec![sq(-5.0, -105.0, 10.0)], 6, Some(100.0)); if let RepeatKind::Radial { radius, .. } = &mut r.kind { *radius = 50.0; } let m = r.transforms()[0]; assert!((m * Point::new(0.0, -100.0) - Point::new(0.0, -50.0)).hypot() < 1e-9); } #[test] fn grid_repeat_transforms() { let g = RepeatSpec::grid(vec![sq(0.0, 0.0, 10.0)], 5.0, 2.0); let t = g.transforms(); assert_eq!(t.len(), 9); assert!((t[1] * Point::ZERO - Point::new(15.0, 0.0)).hypot() < 1e-9); assert!((t[3] * Point::ZERO - Point::new(0.0, 12.0)).hypot() < 1e-9); assert_eq!(g.bounds(), Some(Rect::new(0.0, 0.0, 40.0, 34.0))); } #[test] fn grid_repeat_flip() { let mut g = RepeatSpec::grid(vec![sq(0.0, 0.0, 10.0)], 0.0, 0.0); if let RepeatKind::Grid { flip_cols, .. } = &mut g.kind { *flip_cols = true; } let t = g.transforms(); // Column 1 is mirrored about the source centre, then moved one tile right. assert!((t[1] * Point::new(0.0, 0.0) - Point::new(20.0, 0.0)).hypot() < 1e-9); } #[test] fn mirror_transforms() { let m = RepeatSpec::mirror(vec![sq(0.0, 0.0, 10.0)], 90.0, 5.0); let t = m.transforms(); assert_eq!(t.len(), 2); assert!((t[1] * Point::new(0.0, 3.0) - Point::new(30.0, 3.0)).hypot() < 1e-9); let b = m.bounds().unwrap(); assert!((b.x1 - 30.0).abs() < 1e-9 && b.x0.abs() < 1e-9 && (b.y1 - 10.0).abs() < 1e-9, "{b:?}"); // Horizontal axis. let h = reflect_about(Point::new(0.0, 10.0), 0.0); assert!((h * Point::new(4.0, 0.0) - Point::new(4.0, 20.0)).hypot() < 1e-9); } #[test] fn repeat_transform_moves_arrangement() { let mut r = RepeatSpec::radial(vec![sq(-5.0, -105.0, 10.0)], 4, Some(100.0)); let before = r.bounds().unwrap(); r.transform(Affine::translate((50.0, 0.0)), false); let after = r.bounds().unwrap(); assert!((after.x0 - before.x0 - 50.0).abs() < 1e-9); let mut m = RepeatSpec::mirror(vec![sq(0.0, 0.0, 10.0)], 90.0, 5.0); m.transform(Affine::rotate(std::f64::consts::FRAC_PI_2), false); let RepeatKind::Mirror { angle, .. } = m.kind else { panic!() }; assert!((angle.rem_euclid(180.0) - 0.0).abs() < 1e-9 || (angle.rem_euclid(180.0) - 180.0).abs() < 1e-9, "{angle}"); } #[test] fn serde_roundtrip() { let mut def = PatternDef::new("Dots", vec![sq(0.0, 0.0, 10.0)]); def.tile_type = TileType::HexByRow; let s = serde_json::to_string(&def).unwrap(); assert_eq!(serde_json::from_str::(&s).unwrap(), def); let n = Node::new(NodeId(5), NodeKind::Repeat(RepeatSpec::grid(vec![sq(0.0, 0.0, 10.0)], 1.0, 2.0))); let s = serde_json::to_string(&n).unwrap(); assert_eq!(serde_json::from_str::(&s).unwrap(), n); // Old pattern paints (no xf) still load. let p: Paint = serde_json::from_str(r#"{"type":"pattern","pattern":"Dots"}"#).unwrap(); assert_eq!(p, pattern_paint("Dots")); } }