///|
/// Placement policies for repeating a motif across a rectangular surface.
pub(all) enum TileMode {
Repeat
MirrorX
MirrorY
MirrorBoth
RotateQuarter
} derive(Eq, Debug, ToJson)
///|
/// A bounded tile surface with deterministic cell geometry.
pub(all) struct TileGrid {
bounds : Rect
tile_width : Double
tile_height : Double
columns : Int
rows : Int
mode : TileMode
} derive(Eq, Debug, ToJson)
///|
/// A single tile cell and its local-to-surface transform.
pub(all) struct TileCell {
row : Int
column : Int
bounds : Rect
transform : Matrix2D
} derive(Eq, Debug, ToJson)
///|
/// Reports how well a point set joins at the edges of a tile.
pub(all) struct SeamReport {
horizontal_error : Double
vertical_error : Double
sample_count : Int
periodic : Bool
} derive(Eq, Debug, ToJson)
///|
/// Creates a tile grid. Invalid dimensions collapse to a safe empty surface.
pub fn TileGrid::new(
bounds : Rect,
tile_width : Double,
tile_height : Double,
mode? : TileMode = Repeat,
) -> TileGrid {
let width = tile_width.max(0.0)
let height = tile_height.max(0.0)
let columns = if width <= 0.0 {
0
} else {
(bounds.width / width + 0.999999).to_int()
}
let rows = if height <= 0.0 {
0
} else {
(bounds.height / height + 0.999999).to_int()
}
{ bounds, tile_width: width, tile_height: height, columns, rows, mode }
}
///|
pub fn TileGrid::with_counts(
self : TileGrid,
columns : Int,
rows : Int,
) -> TileGrid {
{ ..self, columns: columns.max(0), rows: rows.max(0) }
}
///|
pub fn TileGrid::cell_count(self : TileGrid) -> Int {
self.columns * self.rows
}
///|
pub fn TileGrid::surface_size(self : TileGrid) -> Vec2 {
Vec2::new(self.bounds.width, self.bounds.height)
}
///|
fn tile_origin(grid : TileGrid, row : Int, column : Int) -> Vec2 {
Vec2::new(
grid.bounds.x + column.to_double() * grid.tile_width,
grid.bounds.y + row.to_double() * grid.tile_height,
)
}
///|
fn tile_center(grid : TileGrid, row : Int, column : Int) -> Vec2 {
let origin = tile_origin(grid, row, column)
Vec2::new(origin.x + grid.tile_width / 2.0, origin.y + grid.tile_height / 2.0)
}
///|
fn mirror_transform(grid : TileGrid, row : Int, column : Int) -> Matrix2D {
let origin = tile_origin(grid, row, column)
let x = origin.x
let y = origin.y
let mirror_x = match grid.mode {
MirrorX => column % 2 == 1
MirrorBoth => column % 2 == 1
_ => false
}
let mirror_y = match grid.mode {
MirrorY => row % 2 == 1
MirrorBoth => row % 2 == 1
_ => false
}
let sx = if mirror_x { -1.0 } else { 1.0 }
let sy = if mirror_y { -1.0 } else { 1.0 }
Matrix2D::translate(
if mirror_x {
x + grid.tile_width
} else {
x
},
if mirror_y {
y + grid.tile_height
} else {
y
},
).multiply(Matrix2D::scale(sx, sy))
}
///|
fn cell_transform(grid : TileGrid, row : Int, column : Int) -> Matrix2D {
match grid.mode {
RotateQuarter => {
let center = tile_center(grid, row, column)
Matrix2D::translate(center.x, center.y)
.multiply(
Matrix2D::rotate(
if (row + column) % 2 == 0 {
0.0
} else {
@math.PI / 2.0
},
),
)
.multiply(Matrix2D::translate(-center.x, -center.y))
.multiply(
Matrix2D::translate(
tile_origin(grid, row, column).x,
tile_origin(grid, row, column).y,
),
)
}
_ => mirror_transform(grid, row, column)
}
}
///|
/// Enumerates tile cells in row-major order.
pub fn TileGrid::cells(self : TileGrid) -> Array[TileCell] {
let result = []
for row in 0.. Array[Matrix2D] {
let result = []
for cell in self.cells() {
result.push(cell.transform)
}
result
}
///|
/// Transforms one path into every tile cell.
pub fn tile_path(path : Path, grid : TileGrid) -> Array[Path] {
let result = []
for transform in grid.transforms() {
result.push(path.transform(transform))
}
result
}
///|
/// Stamps a motif into every tile cell while preserving its style.
pub fn tile_motif(motif : Motif, grid : TileGrid) -> Array[Motif] {
let result = []
for cell in grid.cells() {
let (x, y) = cell.transform.transform_point(motif.anchor.x, motif.anchor.y)
let quarter_turn = match grid.mode {
RotateQuarter =>
if (cell.row + cell.column) % 2 == 0 {
0.0
} else {
@math.PI / 2.0
}
_ => 0.0
}
result.push(motif.placed(x, y, motif.scale, motif.rotation + quarter_turn))
}
result
}
///|
/// Renders a repeated path as an SVG document using the supplied style.
pub fn tile_document(
path : Path,
style : SvgStyle,
grid : TileGrid,
) -> SvgDocument {
let document = SvgDocument::new(grid.bounds.width, grid.bounds.height)
for placed in tile_path(path, grid) {
document.add(SvgElement::SvgPath(placed, style))
}
document
}
///|
/// Wraps a point into a tile-local coordinate system.
pub fn wrap_point(point : Vec2, width : Double, height : Double) -> Vec2 {
if width <= 0.0 || height <= 0.0 {
point
} else {
let wx = point.x % width
let wy = point.y % height
Vec2::new(
if wx < 0.0 {
wx + width
} else {
wx
},
if wy < 0.0 {
wy + height
} else {
wy
},
)
}
}
///|
/// Wraps all points into a tile, useful before serializing a motif template.
pub fn wrap_points(
points : Array[Vec2],
width : Double,
height : Double,
) -> Array[Vec2] {
let result = []
for point in points {
result.push(wrap_point(point, width, height))
}
result
}
///|
/// Returns the shortest distance between two points on a periodic surface.
pub fn torus_distance(
left : Vec2,
right : Vec2,
width : Double,
height : Double,
) -> Double {
if width <= 0.0 || height <= 0.0 {
left.distance(right)
} else {
let dx = (left.x - right.x).abs()
let dy = (left.y - right.y).abs()
let wrapped_x = dx.min(width - dx)
let wrapped_y = dy.min(height - dy)
Vec2::new(wrapped_x, wrapped_y).length()
}
}
///|
/// Measures opposite-edge point pairs for a periodic motif template.
pub fn seam_report(
points : Array[Vec2],
width : Double,
height : Double,
tolerance? : Double = 0.001,
) -> SeamReport {
if points.length() < 2 || width <= 0.0 || height <= 0.0 {
{
horizontal_error: 0.0,
vertical_error: 0.0,
sample_count: points.length(),
periodic: true,
}
} else {
let mut horizontal = 0.0
let mut vertical = 0.0
for point in points {
let wrapped = wrap_point(point, width, height)
horizontal = horizontal.max((point.x - wrapped.x).abs())
vertical = vertical.max((point.y - wrapped.y).abs())
}
let threshold = tolerance.max(0.0)
{
horizontal_error: horizontal,
vertical_error: vertical,
sample_count: points.length(),
periodic: horizontal <= threshold && vertical <= threshold,
}
}
}
///|
/// Produces points repeated in a rectangular tile neighborhood.
pub fn repeat_points(
points : Array[Vec2],
width : Double,
height : Double,
radius : Int,
) -> Array[Vec2] {
let result = []
if width > 0.0 && height > 0.0 && radius >= 0 {
for row in -radius..<=radius {
for column in -radius..<=radius {
let offset = Vec2::new(
column.to_double() * width,
row.to_double() * height,
)
for point in points {
result.push(point.add(offset))
}
}
}
}
result
}
///|
/// Produces a path with translated copies around a tile boundary.
pub fn repeat_path(
path : Path,
width : Double,
height : Double,
radius : Int,
) -> Array[Path] {
let result = []
if width > 0.0 && height > 0.0 && radius >= 0 {
for row in -radius..<=radius {
for column in -radius..<=radius {
let transform = Matrix2D::translate(
column.to_double() * width,
row.to_double() * height,
)
result.push(path.transform(transform))
}
}
}
result
}
///|
/// Builds a seam-safe diagonal stripe tile.
pub fn diagonal_stripe_tile(
width : Double,
height : Double,
spacing : Double,
stroke : Double,
) -> Array[Path] {
let result = []
ignore(stroke)
if width > 0.0 && height > 0.0 && spacing > 0.0 {
let count = ((width + height) / spacing).to_int() + 2
for i in 0.. Array[Path] {
let result = []
if width > 0.0 && height > 0.0 && brick_width > 0.0 && brick_height > 0.0 {
let columns = (width / brick_width).to_int() + 1
let rows = (height / brick_height).to_int() + 1
for row in 0.. Array[Path] {
let result = []
if bands > 0 && width > 0.0 && height > 0.0 {
let step_x = width / bands.to_double()
let step_y = height / bands.to_double()
for i in 0.. Array[Vec2] {
let result = []
if columns > 0 && rows > 0 {
let sx = width / columns.to_double()
let sy = height / rows.to_double()
for row in 0.. Int {
let mut value = grid.columns * 31 + grid.rows
value = value * 31 + grid.tile_width.to_int()
value = value * 31 + grid.tile_height.to_int()
match grid.mode {
Repeat => value
MirrorX => value + 1
MirrorY => value + 2
MirrorBoth => value + 3
RotateQuarter => value + 4
}
}