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

# -------------------------------------------------------------------
# Polyèdres réguliers convexes
# Marc Lorenzi - 4 juillet 2018
# -------------------------------------------------------------------

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

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

SIZE = 350              # La taille en pixels de chaque graphique
ZOOM = 5 * SIZE / 4     # Facteur de zoom
Z0 = 3                  # Position de l'observateur
Z1 = 2                  # Position du plan de projection
BACKGROUND = '#FFFFFF'  # Couleur de fond des graphiques
LINE_COLOR = '#000000'  # Couleur des arêtes
NUMBER_COLOR = '#FF0000'# Couleur des numéros des sommets
FONTSIZE = 14           # Taille de la police de caractères

# -------------------------------------------------------------------
# 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 Polyedre:

    # Constructeur
    def __init__(self, sommets, aretes, faces):
        self.nb_sommets = len(sommets)
        self.sommets = sommets
        self.aretes = aretes
        self.faces = faces
        
    # Projection centrale. Renvoie une liste de points du plan
    def project(self, omega, theta, **args):
        s = []
        for k in range(len(self.sommets)):
            p = rotation(self.sommets[k], 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, omega=(1, 0, 0), theta=0):
        s = self.project(omega, theta)
        for (i, j) in self.aretes:
            canvas.create_line(s[i][0],s[i][1],s[j][0],s[j][1], 
                fill=LINE_COLOR, width=2)
        for i in range(len(s)):
            x, y = s[i]
            canvas.create_text(x-15, y-15, text=str(i), 
                fill=NUMBER_COLOR, font="{helvetica} " + str(FONTSIZE))

# -------------------------------------------------------------------
# Les 5 polyèdres convexes réguliers

def tetraedre():
    s = [(1,1,1),(1,-1,-1),(-1,1,-1),(-1,-1,1)]
    r = 1 / math.sqrt(3)
    s = [(r*x, r*y, r*z) for (x, y, z) in s]
    return Polyedre(s,
    [(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)],
    [[0,1,3],[0,1,2],[0,2,3],[1,2,3]]
)

def cube():
    s = [(-1,-1,-1),(-1,-1,1),(-1,1,-1),(-1,1,1),
         (1,-1,-1),(1,-1,1),(1,1,-1),(1,1,1)]
    r = 1 / math.sqrt(3)
    s = [(r*x, r*y, r*z) for (x, y, z) in s]
    return Polyedre(s,
    [(0,1),(0,2),(0,4),(1,3),
     (1,5),(2,3),(2,6),(3,7),
     (4,5),(4,6),(5,7),(6,7)],
    [[0,1,5,4],[0,2,6,4],[0,1,3,2],[2,6,7,3],[4,5,7,6],[1,5,7,3]]
)

def octaedre():
    return Polyedre(
    [(-1,0,0),(1,0,0),(0,-1,0),(0,1,0),(0,0,-1),(0,0,1)],
    [(0,2),(0,3),(0,4),(0,5),
     (1,2),(1,3),(1,4),(1,5),
     (2,4),(4,3),(3,5),(5,2)],
    [[2,0,4],[2,1,4],[2,0,5],[2,1,5],[3,0,4],[3,0,5],[3,1,5],[3,1,4]]
)

def icosaedre():
    phi = (1 + math.sqrt(5)) / 2
    s = [(0,-1,-phi),(0,-1,phi),(0,1,-phi),(0,1,phi),
         (-1,-phi,0),(-1,phi,0),(1,-phi,0),(1,phi,0),
         (-phi,0,-1),(-phi,0,1),(phi,0,-1),(phi,0,1)]

    r = 1 / math.sqrt(2 + phi)
    s = [(r*x, r*y, r*z) for (x, y, z) in s]
    aretes = [(0,2),(0,8),(0,4),(0,6),(0,10),
              (2,8),(8,4),(4,6),(6,10),(10,2),
              (3,1),(3,9),(3,5),(3,7),(3,11),
              (1,9),(9,5),(5,7),(7,11),(11,1),
              (1,4),(4,9),(9,8),(8,5),(5,2),
              (2,7),(7,10),(10,11),(11,6),(6,1)]
    faces = [[1,11,6],[1,6,4],[1,4,9],[1,9,3],[1,3,11],
             [2,10,0],[2,0,8],[2,8,5],[2,5,7],[2,7,10],
             [7,10,11],[10,11,6],[10,0,6],[6,4,0],[0,8,4],
             [4,9,8],[8,5,9],[9,3,5],[5,7,3],[3,11,7]]
    return Polyedre(s, aretes, faces)

def dodecaedre():
    phi = (1 + math.sqrt(5)) / 2
    s = [(-1,-1,-1),(-1,-1,1),(-1,1,-1),(-1,1,1),
         (1,-1,-1),(1,-1,1),(1,1,-1),(1,1,1),
         (0,-1/phi,-phi),(0,-1/phi,phi),(0,1/phi,-phi),(0,1/phi,phi),
         (-1/phi,-phi,0),(-1/phi,phi,0),(1/phi,-phi,0),(1/phi,phi,0),
         (-phi,0,-1/phi),(-phi,0,1/phi),(phi,0,-1/phi),(phi,0,1/phi)]
    r = 1 / math.sqrt(3)
    s = [(r*x, r*y, r*z) for (x, y, z) in s]
    aretes = [(1,9),(9,5),(5,14),(14,12),(12,1),
              (5,19),(19,7),(7,11),(11,9),(11,3),
              (3,17),(17,1),(17,16),(16,0),(0,12),
              (3,13),(13,2),(2,16),(7,15),(15,13),
              (15,6),(6,10),(10,2),(10,8),(8,0),
              (8,4),(4,14),(18,4),(18,6),(18,19)]
    faces = [[5,19,18,4,14],[5,14,12,1,9],[5,9,11,7,19],
             [0,12,1,17,16],[1,17,3,11,9],[3,13,15,7,11],
             [7,19,18,6,15],[6,18,4,8,10],[0,8,4,14,12],
             [0,8,10,2,16],[2,16,17,3,13],[2,13,15,6,10]]
    return Polyedre(s, aretes, faces)

# -------------------------------------------------------------------
# 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, omega, theta):
    alpha, beta, gamma = omega
    x, y, z = u
    c = math.cos(theta)
    s = math.sin(theta)
    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)

# -------------------------------------------------------------------
# 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(p, zoom=ZOOM, trans=(SIZE / 2, SIZE / 2), z0=Z0, z1=Z1):
    x, y, z = p
    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.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.onLeftRelease)
        self.c.bind('<ButtonRelease-2>', self.onRightRelease)

    # 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    

    # Mouvement de la souris
    def onMove(self, evt):
        if self.leftClick or self.rightClick:
            dx = evt.x - self.x
            dy = evt.y - self.y
            if self.leftClick:
                r = math.sqrt(dx * dx + dy * dy)
                if r == 0: return
                omega = (-dy / r, dx / r, 0)
            else:
                r = math.sqrt(dx * dx)
                if r == 0: return
                omega = (0, 0, dx / r)
            theta = r / SIZE * 4
            self.c.delete("all")
            self.poly.draw(self.c, omega, theta)
            self.x += dx
            self.y += dy

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

    # Bouton droit relâché
    def onRightRelease(self, evt):
        self.rightClick = 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('Polyèdres')
    root.resizable(False, False)
    Label(f, text='Tétraèdre').grid(column=0,row=0, sticky=E+W)
    Graph(f, tetraedre()).grid(column=0, row=1)
    Label(f, text='Cube').grid(column=1,row=0, sticky=E+W)
    Graph(f, cube()).grid(column=1, row=1)
    Label(f, text='Octaèdre').grid(column=2,row=0, sticky=E+W)
    Graph(f, octaedre()).grid(column=2, row=1)
    Label(f, text='Dodécaèdre').grid(column=0,row=3, sticky=E+W)
    Graph(f, dodecaedre()).grid(column=0, row=2)
    Label(f, text='Icosaèdre').grid(column=1,row=3, sticky=E+W)
    Graph(f, icosaedre()).grid(column=1, row=2)
    Label(f, text='clic gauche: rot xOy - clic droit: rot Oz').grid(column=2,row=3, sticky=E+W)
    root.mainloop()

