
# Tile.py
# Edited by Andrew Davison, ad@coe.psu.ac.th, April 2025
'''
  A tile class which defines a group of lines (a Matplotlib path)
  in a unit square). The tile can be transformed using
  Affine2D operations, and tiles can be composed together by
  using compound paths.

  For details, see:
    Programming with Escher
    Massimo Santini
    https://mapio.github.io/programming-with-escher/

  This is a slightly simplified version of the code at:
    https://github.com/mapio/programming-with-escher/tree/master

  Implementing ideas from:
    "Functional Geometry", Peter Henderson, Oct 2002
     https://eprints.soton.ac.uk/257577/1/funcgeo2.pdf
'''

import math

from matplotlib import pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch
from matplotlib.transforms import Affine2D

from svgpath2mpl import parse_path
   # https://pypi.org/project/svgpath2mpl/
   # pip install svgpath2mpl
  

class Tile(object):

  def __init__(self, path=None):
    if not path:
      path=Path([(0,0)])
    self.path=path    # the lines making up the tile drawing


  @classmethod
  def read(cls, fnm):
    # create a tile from the path data in 'fnm.path'
    with open(fnm+".path", 'r') as f:
      svgData=f.read()
      path=parse_path(svgData)
      return cls(path.transformed(
          Affine2D().scale(1,-1).translate(0,1)))


  @staticmethod
  def union(tile0, tile1):
    # composition of two tiles using a compound path
    return Tile(Path.make_compound_path(tile0.path, tile1.path))


  @staticmethod
  def transform(tile, transform):
    # apply an Affine2D tranformation to the path
    return Tile(tile.path.transformed(transform))


  def show(self, ax):
    # draw path outline in black as a Matplotlib patch
    ax.add_patch(PathPatch(self.path, fill=False))


# -------------- Affine2D transformations ------------

def flip(tile):
  # reflect in the y-axis
  return Tile.transform(tile, 
      Affine2D().scale(-1, 1).translate(1,0))


def rot(tile):
  # rotate 90 degrees counter-clockwise
  return Tile.transform(tile, 
      Affine2D().rotate_deg(90).translate(1,0))


def rot45(tile):
  sc=1/math.sqrt(2)
  return Tile.transform(tile,
    Affine2D().rotate_deg(45).scale(sc, sc).translate(1/2,1/2)
  )


# --------- Basic Tile composition ---------------
# using Tile.union and transformations

def over(tile0, tile1):
  # place tile0 on top of tile 1
  return Tile.union(tile0, tile1)


def beside(tile0, tile1, n=1, m=1):
  '''
    Place tile 0 on the left of tile 1.
    n amd m are optional scaling factors so that
    the new tile is a unit square
  '''
  den = n + m
  return Tile.union(
    Tile.transform(tile0, Affine2D().scale(n/den, 1)),
    Tile.transform(tile1, 
        Affine2D().scale(m/den, 1).translate(n/den, 0))
    )


def above(tile0, tile1, n=1, m=1):
  '''
    Place tile 0 above tile 1.
    n amd m are optional scaling factors so that
    the new tile is a unit square
  '''
  den=n + m
  return Tile.union(
    Tile.transform(tile0, 
       Affine2D().scale(1, n/den).translate(0, m/den)),
    Tile.transform(tile1, Affine2D().scale(1, m/den))
  )


# more complex compositions, defined using the basic ones

def quartet(p, q, r, s):
  ''' a unit grid    p  q
                     r  s
  '''
  return above(beside(p, q), beside(r, s))


def nonet(p, q, r, s, t, u, v, w, x):
  ''' a unit grid    p  q  r
                     s  t  u
                     v  w  x
  '''
  return above(
     beside(p, beside(q, r), 1,2),
     above( beside(s, beside(t, u), 1,2),
            beside(v, beside(w, x), 1,2) ), 1,2)


# --------------------------------
if __name__ == "__main__":
  '''
  blank = Tile()
  fish = Tile.read('fish')
  edge = Tile.read('edge')
  triangle = Tile.read('triangle')
  fish.show(ax)
  '''
  f = Tile.read('f')

  _, axes = plt.subplots(2, 4, # 2 rows, 4 columns
               figsize=(7, 3))
  # top row  
  ax = axes[0,0]
  ax.set_aspect('equal')
  f.show(ax)
  ax.set_title("f")

  ax = axes[0,1]
  ax.set_aspect('equal')
  f1 = rot(f)
  f1.show(ax)
  ax.set_title("rot(f)")

  ax = axes[0,2]
  ax.set_aspect('equal')
  f2 = flip(f)
  f2.show(ax)
  ax.set_title("flip(f)")

  ax = axes[0,3]
  ax.set_aspect('equal')
  f3 = rot(flip(f))
  f3.show(ax)
  ax.set_title("rot(flip(f))")

  # bottom row  
  ax = axes[1,0]
  ax.set_aspect('equal')
  f4 = above(f,f)
  f4.show(ax)
  ax.set_title("above(f,f)")

  ax = axes[1,1]
  ax.set_aspect('equal')
  f5 = beside(f,f)
  f5.show(ax)
  ax.set_title("beside(f,f)")

  ax = axes[1,2]
  ax.set_aspect('equal')
  f6 = above(beside(f,f),f)
  f6.show(ax)
  ax.set_title("above(beside(f,f),f)")

  ax = axes[1,3]
  ax.set_aspect('equal')
  f7 = quartet(f, f, f, f)
  f7.show(ax)
  ax.set_title("quartet(f,f,f,f)")

  plt.tight_layout()
  plt.show()