# -*- coding:UTF-8 -*-

# -------------------------------------------------------------------
# Polytopes en dimension 4
# Marc Lorenzi - 4 juillet 2018
# -------------------------------------------------------------------

# -------------------------------------------------------------------
from tkinter import *
import math

# -------------------------------------------------------------------
# Paramètres globaux

SIZE = 350              # La taille en pixels de chaque graphique
ZOOM =  0.4 * SIZE      # Facteur de zoom
Z0 = 3                  # Position de l'observateur
Z1 = 0                  # Position du plan de projection
T0 = 3
T1 = 0
BACKGROUND = '#FFFFFF'  # Couleur de fond des graphiques
LINE_COLOR = '#000000'  # Couleur des arêtes

# -------------------------------------------------------------------
# La classe des polyèdres

# Un polyèdre est donné par :
#   - la liste des coordonnées de ses sommets
#   - la liste des arêtes, de la forme (i, j) où i et j sont les
# numéros des extrémités de l'arête
#   - la liste des faces, de la forme (i0, i1, ..., ir)$ où les 
# ij sont les sommets de la face, dans l'ordre.

class Polytope:

    # Constructeur
    def __init__(self, sommets, aretes):
        self.nb_sommets = len(sommets)
        self.sommets = sommets
        self.aretes = aretes
        self.proj = {}
        
    # Projection centrale. Renvoie une liste de points du plan
    def project(self, omega, theta, n, **args):
        s = []
        for k in range(len(self.sommets)):
            p = rotation(self.sommets[k], n, omega, theta)
            self.sommets[k] = p
            p = projection(p, **args)
            s.append(p)
        return s

    # Dessiner le polyèdre sur un Canvas
    def draw(self, canvas, n=0, omega=(1, 0, 0), theta=0):
        s = self.project(omega, theta, n)
        if self.proj == {}: self.drawCanvas(s, canvas)
        else: self.updateCanvas(s, canvas)

    def drawCanvas(self, s, canvas):
        for (i, j) in self.aretes:
            self.proj[(i, j)]=canvas.create_line(s[i][0],s[i][1],s[j][0],s[j][1], 
                                    fill=LINE_COLOR, width=1)

    def updateCanvas(self, s, canvas):
        for (i, j) in self.aretes:
            canvas.coords(self.proj[(i, j)], s[i][0], s[i][1], s[j][0], s[j][1])


def distance(A, B):
    a, b, c, d = A
    x, y, z, t = B
    return math.sqrt((x-a)**2+(y-b)**2+(z-c)**2+(t-d)**2)

def calcul_aretes(s):
    ar = []
    n = len(s)
    d = distance(s[0], s[1])
    for j in range(1, n):
        d1 = distance(s[0], s[j])
        if d1 < d: d = d1
    for i in range(n):
        voisi = []
        for j in range(i+1, n):
            if distance(s[i], s[j]) <= d + 1e-10: voisi.append(j)
        ar += [(i, j) for j in voisi if i < j]
    return ar

# Permutations

perm_paires = [[0,1,2,3], [1,0,3,2], [3,2,1,0], [2,3,0,1],
               [1,2,0,3], [2,0,1,3], [0,2,3,1], [0,3,1,2],
               [2,1,3,0], [3,1,0,2], [1,3,2,0], [3,0,2,1]]

perm_impaires = [[1,0,2,3], [0,1,3,2], [3,2,0,1], [2,3,1,0],
                [0,2,1,3], [2,1,0,3], [1,2,3,0], [1,3,0,2],
                [2,0,3,1], [3,0,1,2], [0,3,2,1], [3,1,2,0]]

perms = perm_paires + perm_impaires

def permutations_paires(u):
    return [(u[s[0]],u[s[1]],u[s[2]],u[s[3]]) for s in perm_paires]

def permutations(u):
    return [(u[s[0]],u[s[1]],u[s[2]],u[s[3]]) for s in perms]

# -------------------------------------------------------------------
# Les 6 polytopes en dimension 4

# Le simplexe

def simplexe():
    s = [(1/math.sqrt(10), 1/math.sqrt(6), 1/math.sqrt(3), 1),
          (1/math.sqrt(10), 1/math.sqrt(6), 1/math.sqrt(3), -1),
          (1/math.sqrt(10), 1/math.sqrt(6), -2/math.sqrt(3), 0),
          (1/math.sqrt(10), -math.sqrt(3/2), 0, 0),
          (-2*math.sqrt(2/5), 0, 0, 0)]
    r = math.sqrt(8/5)
    s = [(a/r,b/r,c/r,d/r) for (a,b,c,d) in s]
    return Polytope(s, calcul_aretes(s))

# L'hypercube

def hypercube():
    l = [-1, 1]
    s = [(a,b,c,d) for a in l for b in l for c in l for d in l]
    s = [(a/2,b/2,c/2,d/2) for (a,b,c,d) in s]
    return Polytope(s, calcul_aretes(s))

# Le 16

def poly16():
    s = [(1, 0, 0, 0), (-1, 0, 0, 0), (0, 1, 0, 0), (0, -1, 0, 0),
         (0, 0, 1, 0), (0, 0, -1, 0), (0, 0, 0, 1), (0, 0, 0, -1)]
    #s = [(a/r,b/r,c/r,d/r) for (a,b,c,d) in s]
    return Polytope(s, calcul_aretes(s))

# Le 24

def sommets24():
    s = [(a,b,0,0) for a in [-1,1] for b in [-1,1]]
    s += [(a,0,b,0) for a in [-1,1] for b in [-1,1]]
    s += [(a,0,0,b) for a in [-1,1] for b in [-1,1]]
    s += [(0,a,b,0) for a in [-1,1] for b in [-1,1]]
    s += [(0,a,0,b) for a in [-1,1] for b in [-1,1]]
    s += [(0,0,a,b) for a in [-1,1] for b in [-1,1]]
    r = math.sqrt(2)
    s = [(a/r,b/r,c/r,d/r) for (a,b,c,d) in s]
    return s

def poly24():
    s = sommets24()
    return Polytope(s, calcul_aretes(s))

# Le 600

def sommets600():
    phi = (1 + math.sqrt(5)) / 2
    l = [-0.5, 0.5]
    s = [(a,b,c,d) for a in l for b in l for c in l for d in l]
    s += [(a, 0, 0, 0) for a in [-1, 1]]
    s += [(0, a, 0, 0) for a in [-1, 1]]
    s += [(0, 0, a, 0) for a in [-1, 1]]
    s += [(0, 0, 0, a) for a in [-1, 1]]
    
    q = (phi, 1, 1/phi, 0)
    s1 = permutations_paires(q)

    l = [-0.5, 0.5]
    s2 = [(a,b,c,d) for a in l for b in l for c in l for d in l]
    s3 = list(set([(a*x,b*y,c*z,d*t) for (a,b,c,d) in s2 for (x,y,z,t) in s1]))
    s += s3

    r = 1
    s = [(a/r,b/r,c/r,d/r) for (a,b,c,d) in s]
    return s


def poly600():
    s = sommets600()
    return Polytope(s, calcul_aretes(s))


# Le 120

def sommets120():
    phi = (1 + math.sqrt(5)) / 2
    s = []
    for a in [-1, 1]:
        for b in [-1, 1]:
            for c in [-1,1]:
                for d in [-1,1]:
                    s += permutations((0, 0, 2*c, 2*d))
                    s += permutations((a,b,c,d*math.sqrt(5)))
                    s += permutations((a/phi**2,b*phi,c*phi,d*phi))
                    s += permutations((a/phi,b/phi,c/phi,d*phi**2))
                    s += permutations_paires((0, b/phi**2, c, d*phi**2))
                    s += permutations_paires((0,b/phi,c*phi,d*math.sqrt(5)))
                    s += permutations_paires((a/phi,b,c*phi,d*2))
    s = list(set(s))
    r = 2 * math.sqrt(2)
    s = [(a/r,b/r,c/r,d/r) for (a,b,c,d) in s]
    return s

def poly120():
    s = sommets120()
    return Polytope(s, calcul_aretes(s))



# -------------------------------------------------------------------
# Rotation d'axe orienté par le vecteur unitaire omega,
# et d'angle theta. On utilise la formule d'Euler-Rodrigues :

# f(u) = cos(theta) u +(1-cos theta) <omega, u> omega
#        + sin theta (omega x u)

def rotation(u, n, omega, theta):
    c = math.cos(theta)
    s = math.sin(theta)
    alpha, beta, gamma = omega
    if n == 0: 
        x, y, z, p = u
        ps = alpha * x + beta * y + gamma * z
        X = c * x + (1 - c) * ps * alpha + s * (beta * z - gamma * y)
        Y = c * y + (1 - c) * ps * beta  + s * (gamma * x - alpha * z)
        Z = c * z + (1 - c) * ps * gamma + s * (alpha * y - beta * x)
        return (X, Y, Z, p)
    elif n == 1: 
        if beta == 0: return u
        else:
            d = beta / abs(beta)
            s = s * d
            x, y, z, t = u
            #X = c * x - s * z
            X = x
            Y = c * y - s * t
            #Z = s * x + c * z
            Z = z
            T = s * y + c * t
            return (X, Y, Z, T)


# -------------------------------------------------------------------
# Projection centrale

# On projette sur le plan d'équation z = z1
# L'observateur est situé au point (0, 0, z0)
# Après projection, on multiplie par un facteur de zoom puis on translate

def projection_stereo(p):
    x, y, z, t = p


def projection(p, zoom=ZOOM, trans=(SIZE / 2, SIZE / 2), z0=Z0, z1=Z1, t0=T0, t1=T1):
    x, y, z, t = p
    nu = (t1 - t0) / (t - t0)
    x = nu * x
    y = nu * y
    z = nu * z
    mu = zoom * (z1 - z0) / (z - z0)
    return (mu * x + trans[0], mu * y + trans[1])

# -------------------------------------------------------------------
# Fenêtre graphique

class Graph(Frame):

    # Constructeur
    def __init__(self, parent, poly):
        Frame.__init__(self, parent)
        self.c = Canvas(self, width=SIZE, height=SIZE, bg=BACKGROUND, 
            highlightthickness=0)
        self.c.grid()
        self.poly = poly
        self.x = None
        self.y = None
        self.leftClick = False
        self.rightClick = False
        self.shiftClick = False
        self.poly.draw(self.c)
        self.add_callbacks()

    def add_callbacks(self):
        self.c.bind('<Button-1>', self.onLeftClick)
        self.c.bind('<Button-2>', self.onRightClick)
        self.c.bind('<Motion>', self.onMove)
        self.c.bind('<ButtonRelease-1>', self.onRelease)
        self.c.bind('<ButtonRelease-2>', self.onRelease)
        self.c.bind('<Shift-Button-1>', self.onShiftClick)
        self.c.bind('<Shift-ButtonRelease-1>', self.onRelease)


    # Appui sur le bouton gauche
    def onLeftClick(self, evt):
        self.x = evt.x
        self.y = evt.y
        self.leftClick = True

    # Appui sur le bouton droit
    def onRightClick(self, evt):
        self.x = evt.x
        self.y = evt.y
        self.rightClick = True

    # Appui sur shift + bouton gauche
    def onShiftClick(self, evt):
        self.x = evt.x
        self.y = evt.y
        self.shiftClick = True
        
    # Mouvement de la souris
    def onMove(self, evt):
        if self.leftClick or self.rightClick or self.shiftClick:
            dx = evt.x - self.x
            dy = evt.y - self.y
            
            if self.leftClick or self.shiftClick: n = 0
            elif self.rightClick: n = 1
            r = math.sqrt(dx * dx + dy * dy)
            if r == 0: return
            omega = (-dy / r, dx / r, 0)
            theta = 4 * r / SIZE
            if self.leftClick: self.poly.draw(self.c, 0, omega, theta)
            elif self.shiftClick:
                if dx == 0: return
                else:
                    if omega[1] > 0:
                        self.poly.draw(self.c, 0, (0,0,1), 2 * theta)
                    else:
                        self.poly.draw(self.c, 0, (0,0,-1), 2 * theta)
            else: self.poly.draw(self.c, 1, omega, theta)
            self.x += dx
            self.y += dy

    # Bouton gauche relâché  
    def onRelease(self, evt):
        self.leftClick = False
        self.rightClick = False
        self.shiftClick = False



# -------------------------------------------------------------------
# Les 5 polyèdres dans une même fenêtre

if __name__ == '__main__':
    root = Tk()
    f = Frame(root, bg=BACKGROUND)
    f.grid()
    root.title('Polytopes')
    root.resizable(False, False)
    Label(f, text='Simplexe').grid(column=0,row=0, sticky=E+W)
    Graph(f, simplexe()).grid(column=0, row=1)
    Label(f, text='Hypercube').grid(column=1,row=0, sticky=E+W)
    Graph(f, hypercube()).grid(column=1, row=1)
    Label(f, text='16-Cell').grid(column=2,row=0, sticky=E+W)
    Graph(f, poly16()).grid(column=2, row=1)
    Label(f, text='24-Cell').grid(column=0,row=3, sticky=E+W)
    Graph(f, poly24()).grid(column=0, row=2)
    Label(f, text='120-Cell').grid(column=1,row=3, sticky=E+W)
    Graph(f, poly120()).grid(column=1, row=2)
    Label(f, text='600-Cell').grid(column=2,row=3, sticky=E+W)
    Graph(f, poly600()).grid(column=2, row=2)
    #Label(f, text='clic gauche: rot 3D - clic droit: rot 4D').grid(column=1,row=3, sticky=E+W)
    #Graph(f, poly600()).grid()
    root.mainloop()


