510 lines
20 KiB
Python
510 lines
20 KiB
Python
'''
|
|
Created on 9 mars 2017
|
|
|
|
Copyright 2017 Jean-Marie Mineau, Maxime Keller
|
|
This file is part of "ISN's Cube".
|
|
|
|
"ISN's Cube" is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
"ISN's Cube" is distributed in the hope that it will be useful and
|
|
recreative, but WITHOUT ANY WARRANTY; without even the implied
|
|
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with "ISN's Cube". If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
@author: <mineau.jean.marie@gmail.com>
|
|
|
|
Programme inspiré d'un programme dévellopé par Leonel Machava <leonelmachava@gmail.com>
|
|
http://codeNtronix.com
|
|
'''
|
|
|
|
import sys, math, pygame
|
|
from operator import itemgetter
|
|
|
|
class Point3D:
|
|
def __init__(self, x = 0, y = 0, z = 0):
|
|
self.x, self.y, self.z = float(x), float(y), float(z)
|
|
|
|
def rotationX(self, angle):
|
|
""" Fait pivoter le point autour de l'axe X d'une valeur donnée en degrés. """
|
|
rad = angle * math.pi / 180
|
|
cosa = math.cos(rad)
|
|
sina = math.sin(rad)
|
|
y = self.y * cosa - self.z * sina #Formules d'addition de trigo.
|
|
z = self.y * sina + self.z * cosa
|
|
return Point3D(self.x, y, z)
|
|
|
|
def rotationY(self, angle):
|
|
""" Fait pivoter le point autour de l'axe Y d'une valeur donnée en degrés. """
|
|
rad = angle * math.pi / 180
|
|
cosa = math.cos(rad)
|
|
sina = math.sin(rad)
|
|
z = self.z * cosa - self.x * sina #Formules d'addition de trigo.
|
|
x = self.z * sina + self.x * cosa
|
|
return Point3D(x, self.y, z)
|
|
|
|
def rotationZ(self, angle):
|
|
""" Fait pivoter le point autour de l'axe Z d'une valeur donnée en degrés. """
|
|
rad = angle * math.pi / 180
|
|
cosa = math.cos(rad)
|
|
sina = math.sin(rad)
|
|
x = self.x * cosa - self.y * sina #Formules d'addition de trigo.
|
|
y = self.x * sina + self.y * cosa
|
|
return Point3D(x, y, self.z)
|
|
|
|
def projection(self, largeur_ecran, hauteur_ecran, zoom, distance):
|
|
""" Transforme ce point 3D en point 2D (avec la profondeur conservé). """
|
|
factor = zoom / (distance + self.z)
|
|
x = self.x * factor + largeur_ecran / 2
|
|
y = -self.y * factor + hauteur_ecran / 2
|
|
return Point3D(x, y, self.z)
|
|
|
|
def __eq__(self, point):
|
|
"""opérateur d'égalité."""
|
|
x = abs(self.x - point.x) < 0.01
|
|
y = abs(self.y - point.y) < 0.01
|
|
z = abs(self.z - point.z) < 0.01
|
|
return x and y and z
|
|
|
|
def dans(self, points):
|
|
"""Test si un point est dans une liste de points avec une impressition."""
|
|
for point in points:
|
|
|
|
x = False
|
|
y = False
|
|
z = False
|
|
|
|
if abs(self.x - point.x) < 0.01:
|
|
x = True
|
|
if abs(self.y - point.y) < 0.01:
|
|
y = True
|
|
if abs(self.z - point.z) < 0.01:
|
|
z = True
|
|
|
|
if x and y and z:
|
|
return True
|
|
|
|
return False
|
|
|
|
def copie(self):
|
|
"""Retourne une copie de l'objet."""
|
|
return Point3D(self.x, self.y, self.z)
|
|
|
|
class Peintre:
|
|
"""Objet de tri pour les faces."""
|
|
def __init__(self, LARGEUR_ECRAN = 500, HAUTEUR_ECRAN = 500, ZOOM = 254, DISTANCE = 50, ECART_INTER = 0.01):
|
|
self.LARGEUR_ECRAN = LARGEUR_ECRAN # DISTANCE 10
|
|
self.HAUTEUR_ECRAN = HAUTEUR_ECRAN
|
|
self.ZOOM = ZOOM
|
|
self.DISTANCE = DISTANCE
|
|
self.ECART_INTER = ECART_INTER
|
|
self.coin = []
|
|
self.faces = []
|
|
|
|
def intersection(self, A, B, C, D):
|
|
"""Retourne l'intersection des secgments [point1 point2] et [point3 point4] en 2 dimentions"""
|
|
|
|
# Segments paralleles
|
|
if A.x == B.x and C.x == D.x: return None
|
|
|
|
elif (A.x == B.x):
|
|
c = (C.y - D.y)/(C.x - D.x)
|
|
d = C.y - (c*C.x)
|
|
x = A.x
|
|
y = c*x + d
|
|
|
|
elif (C.x == D.x):
|
|
a = (A.y - B.y)/(A.x - B.x)
|
|
b = A.y - (a*A.x)
|
|
x = C.x
|
|
y = a*x + b
|
|
|
|
else:
|
|
a = (A.y - B.y)/(A.x - B.x)
|
|
b = A.y - (a*A.x)
|
|
c = (C.y - D.y)/(C.x - D.x)
|
|
d = C.y - (c*C.x)
|
|
|
|
if a == c:
|
|
return None # Segment parallèles
|
|
elif a == 0:
|
|
y = B.y
|
|
x = (y - d)/c
|
|
elif c == 0:
|
|
y = D.y
|
|
x = (y - b/a)
|
|
else:
|
|
x = (d - b)/(a - c)
|
|
y = a*x + b
|
|
|
|
pointDansSegment1 = (min(A.x, B.x) + self.ECART_INTER < x < max(A.x, B.x) - self.ECART_INTER) \
|
|
and (min(A.y, B.y) + self.ECART_INTER < y < max(A.y, B.y) - self.ECART_INTER)
|
|
pointDansSegment2 = (min(C.x, D.x) + self.ECART_INTER < x < max(C.x, D.x) - self.ECART_INTER) \
|
|
and (min(C.y, D.y + self.ECART_INTER) < y < max(C.y, D.y) - self.ECART_INTER)
|
|
|
|
if pointDansSegment1 and pointDansSegment2:
|
|
return Point3D(x, y, 0)
|
|
else:
|
|
return None
|
|
|
|
def profondeurIntersection(self, A, B, I):
|
|
"""Retourne le point avec sa profondeur"""
|
|
|
|
x = I.x
|
|
y = I.y
|
|
|
|
if A.z == B.z:
|
|
z = A.z
|
|
return Point3D(x, y, z)
|
|
|
|
a = (A.x - B.x)/(A.z - B.z)
|
|
b = B.x - (a*B.z)
|
|
c = (A.y - B.y)/(A.z - B.z)
|
|
d = B.y - (c*B.z)
|
|
|
|
# Attention grosse formule!
|
|
|
|
denominateur = ((x - self.LARGEUR_ECRAN/2) - a*self.ZOOM)
|
|
if denominateur != 0:
|
|
z = (b*self.ZOOM - (x - self.LARGEUR_ECRAN/2)*self.DISTANCE)/denominateur
|
|
return Point3D(x, y, z)
|
|
|
|
denominateur = (c*self.ZOOM + y - self.HAUTEUR_ECRAN/2)
|
|
if denominateur != 0:
|
|
z = (-d*self.ZOOM - (y - self.HAUTEUR_ECRAN/2)*self.DISTANCE)/denominateur
|
|
return Point3D(x, y, z)
|
|
|
|
denominateur = (y - self.HAUTEUR_ECRAN/2 + (c+d)*self.ZOOM)
|
|
if denominateur != 0:
|
|
z = -(y - self.HAUTEUR_ECRAN/2)*self.DISTANCE/denominateur
|
|
return Point3D(x, y, z)
|
|
|
|
## Formules qui MARCHENT:
|
|
#z = - (y - HAUTEUR_ECRAN/2)*DISTANCE / (y - HAUTEUR_ECRAN/2 + (c+d)*ZOOM)
|
|
#z = (-d*ZOOM - (y - HAUTEUR_ECRAN/2)*DISTANCE)/(c*ZOOM + y - HAUTEUR_ECRAN/2)
|
|
#z = (b*ZOOM - (x - LARGEUR_ECRAN/2)*DISTANCE)/((x - LARGEUR_ECRAN/2) - a*ZOOM)
|
|
|
|
## Formules FAUSSENT z = ((y - HAUTEUR_ECRAN/2)*a/ZOOM + b) / (1- (a/ZOOM)*(y - HAUTEUR_ECRAN/2))
|
|
##z = ((x - LARGEUR_ECRAN/2)*DISTANCE - b*ZOOM)/(a*ZOOM - x - LARGEUR_ECRAN/2)
|
|
z = 0
|
|
return Point3D(x, y, z)
|
|
|
|
def tri(self, faces, intersections, segments, points3D, point2D):
|
|
"""Trie les faces en fonction des points d'intersection"""
|
|
|
|
# pointsSegments contient des listes avec en première valeur le point d'intersection
|
|
# en deuxième et troisième les extrèmitès du segment auquel il appartient.
|
|
# Ces listes vont normalement par paire.
|
|
while intersections:
|
|
# Selectionne deux points d'intersections de même position
|
|
intersection1 = intersections[0]
|
|
intersections = intersections[1:]
|
|
segment1 = segments[0]
|
|
segments = segments[1:]
|
|
for intersection2 in intersections:
|
|
if abs(intersection1.x - intersection2.x) < 0.01 and abs(intersection1.y - intersection2.y) < 0.01:
|
|
segment2 = segments[intersections.index(intersection2)]
|
|
segments.remove(segment2)
|
|
intersections.remove(intersection2)
|
|
break
|
|
|
|
# verifie la validite du point
|
|
if intersection1.x - intersection2.x < 0.01 and intersection1.y - intersection2.y < 0.01:
|
|
|
|
faces1 = []
|
|
faces2 = []
|
|
|
|
for face in faces:
|
|
i = faces.index(face)
|
|
pointsFaces = [points3D[j] for j in face]
|
|
if segment1[0].dans(pointsFaces) and \
|
|
segment1[1].dans(pointsFaces):
|
|
faces1.append(i)
|
|
if segment2[0].dans(pointsFaces) and \
|
|
segment2[1].dans(pointsFaces):
|
|
faces2.append(i)
|
|
|
|
# Nous avons associer deux points d'intersection de profondeur differentes
|
|
# aux faces qui contiennent le segment
|
|
# Il faut que les faces donc le segment est le plus proche soit avans les
|
|
# deux autres.
|
|
interTpm = [intersection1, intersection2]
|
|
facesTpm = [faces1, faces2]
|
|
# Le point le plus éloigné en premier, donc les premieres faces a afficher en premier
|
|
if interTpm[0].z > interTpm[1].z:
|
|
tmp = facesTpm[0]
|
|
facesTpm[0] = facesTpm[1]
|
|
facesTpm[1] = tmp
|
|
# les premieres faces doivent etre avant la limite
|
|
limite = min(facesTpm[0])
|
|
indice1, indice2 = facesTpm[1]
|
|
|
|
if indice1 > limite and indice2 > limite:
|
|
faces1Liste = faces[:limite]
|
|
faces2Liste = faces[limite:]
|
|
faces1Liste.append(faces[min(indice1, indice2)])
|
|
faces1Liste.append(faces[max(indice1, indice2)])
|
|
faces2Liste.remove(faces[indice1])
|
|
faces2Liste.remove(faces[indice2])
|
|
faces = faces1Liste + faces2Liste
|
|
|
|
elif indice1 > limite:
|
|
faces1Liste = faces[:limite]
|
|
faces2Liste = faces[limite:]
|
|
faces1Liste.append(faces[indice1])
|
|
faces2Liste.remove(faces[indice1])
|
|
faces = faces1Liste + faces2Liste
|
|
|
|
elif indice2 > limite:
|
|
faces1Liste = faces[:limite]
|
|
faces2Liste = faces[limite:]
|
|
faces1Liste.append(faces[indice2])
|
|
faces2Liste.remove(faces[indice2])
|
|
faces = faces1Liste + faces2Liste
|
|
|
|
return faces
|
|
|
|
def peintre1(self, faces, points2D):
|
|
"""Algorithme du peintre traditionnel."""
|
|
moy_z = []
|
|
i = 0
|
|
for f in faces:
|
|
z = (points2D[f[0]].z + points2D[f[1]].z + points2D[f[2]].z + points2D[f[3]].z) / 4.0
|
|
moy_z.append([i,z])
|
|
i = i + 1
|
|
|
|
nouvellesFaces = []
|
|
# Trie lessurfaces en utlisant l'algorithme du peintre
|
|
# Les faces les plus éloignées sont tracées avant les plus proches.
|
|
for tmp in sorted(moy_z,key=itemgetter(1),reverse=True):
|
|
indiceFace = tmp[0]
|
|
nouvellesFaces.append(faces[indiceFace])
|
|
|
|
return nouvellesFaces
|
|
|
|
def peintre2(self):
|
|
"""Retourne une liste de surface clasés selon l'algorythme du peintre, ainsi que leur couleur"""
|
|
|
|
points3D = self.coins
|
|
points2D = [i.projection(self.LARGEUR_ECRAN, self.HAUTEUR_ECRAN, self.ZOOM, self.DISTANCE) for i in self.coins]
|
|
faces = self.faces
|
|
intersections = []
|
|
segments = []
|
|
|
|
for face1 in self.faces:
|
|
#Test l'intersection de tous les point de chaque face
|
|
for face2 in self.faces:
|
|
for j in [-1, 0, 1, 2]:
|
|
for i in [-1, 0, 1, 2]:
|
|
|
|
A3D = points3D[face1[j]]
|
|
B3D = points3D[face1[j+1]]
|
|
C3D = points3D[face2[i]]
|
|
D3D = points3D[face2[i+1]]
|
|
|
|
A = points2D[face1[j]]
|
|
B = points2D[face1[j+1]]
|
|
C = points2D[face2[i]]
|
|
D = points2D[face2[i+1]]
|
|
|
|
inter = self.intersection(A, B, C, D)
|
|
if inter:
|
|
intersections.append(self.profondeurIntersection(A3D, B3D, inter))
|
|
segments.append([A3D, B3D])
|
|
intersections.append(self.profondeurIntersection(C3D, D3D, inter))
|
|
segments.append([C3D, D3D])
|
|
|
|
faces = self.peintre1(faces, points2D)
|
|
faces = self.tri(faces, intersections, segments, points3D, points2D)
|
|
return faces
|
|
|
|
|
|
class Cubie3D(Peintre):
|
|
"""Class définissant un cube en 3D"""
|
|
|
|
def __init__(self, pos):
|
|
"""Définit le cube de centre pos et de coté 2"""
|
|
|
|
Peintre.__init__(self)
|
|
|
|
self.centre = Point3D(pos[0], pos[1], pos[2])
|
|
|
|
self.coins = [
|
|
Point3D(pos[0]-2,pos[1]+2,pos[2]+2), # droit haut arrière
|
|
Point3D(pos[0]+2,pos[1]+2,pos[2]+2), # gauche haut arrière
|
|
Point3D(pos[0]-2,pos[1]+2,pos[2]-2), # droit haut avant
|
|
Point3D(pos[0]+2,pos[1]+2,pos[2]-2), # gauche haut avant
|
|
Point3D(pos[0]-2,pos[1]-2,pos[2]+2), # droit bas arrière
|
|
Point3D(pos[0]+2,pos[1]-2,pos[2]+2), # gauche bas arrière
|
|
Point3D(pos[0]-2,pos[1]-2,pos[2]-2), # droit bas avant
|
|
Point3D(pos[0]+2,pos[1]-2,pos[2]-2)] # gauche bas avant
|
|
#Liste des faces, les valeurs sont les indices du point correspondant dans la liste coins.
|
|
self.faces = [
|
|
(0,1,3,2), # Haut
|
|
(4,5,7,6), # Bas
|
|
(0,2,6,4), # Droite
|
|
(1,3,7,5), # Gauche
|
|
(0,1,5,4), # Arrière
|
|
(2,3,7,6)] # Avant
|
|
#Liste des couleurs, leur indices sont les mêmes que ceux de la faces associer.
|
|
self.couleurs = [
|
|
(0,0,0), #(255,0,0), # Haut
|
|
(0,0,0), #(255,70,0), # Bas
|
|
(0,0,0), #(255,255,0), # Gauche
|
|
(0,0,0), #(255,255,255), # Droite
|
|
(0,0,0), #(0,255,0), # Arrière
|
|
(0,0,0)] #(0,0,255)] # Avant
|
|
|
|
self.couleursResolution = []
|
|
|
|
def getMinZ(self):
|
|
"""Retourne la profondeur du point le plus proche."""
|
|
z = self.coins[0].z
|
|
for pt in self.coins:
|
|
if pt.z < z:
|
|
z = pt.z
|
|
return z
|
|
|
|
minZ = property(fget = getMinZ)
|
|
|
|
def rotationX(self, angle):
|
|
""" Fait pivoter le cube autour de l'axe X d'une valeur donnée en degrés. """
|
|
newCoins = []
|
|
for point in self.coins:
|
|
newCoins.append(point.rotationX(angle))
|
|
newCube = self.copie()
|
|
newCube.coins = newCoins
|
|
newCube.centre = self.centre.rotationX(angle)
|
|
return newCube
|
|
|
|
def rotationY(self, angle):
|
|
""" Fait pivoter le cube autour de l'axe Y d'une valeur donnée en degrés. """
|
|
newCoins = []
|
|
for point in self.coins:
|
|
newCoins.append(point.rotationY(angle))
|
|
newCube = self.copie()
|
|
newCube.coins = newCoins
|
|
newCube.coins = newCoins
|
|
newCube.centre = self.centre.rotationY(angle)
|
|
return newCube
|
|
|
|
def rotationZ(self, angle):
|
|
""" Fait pivoter le cube autour de l'axe Z d'une valeur donnée en degrés. """
|
|
newCoins = []
|
|
for point in self.coins:
|
|
newCoins.append(point.rotationZ(angle))
|
|
newCube = self.copie()
|
|
newCube.coins = newCoins
|
|
newCube.centre = self.centre.rotationZ(angle)
|
|
return newCube
|
|
|
|
def getOrientationFace(self, couleur):
|
|
"""Retourn l'orientation de la face de la couleur donnée.
|
|
Elle est donné sous la forme x, y, z, seul l'axe perpendiculaire
|
|
à la face est différent ce 0, et le signe son de quel côté du cube il ce trouve."""
|
|
indiceFace = self.couleurs.index(couleur)
|
|
pointsFace = [(self.coins[i].x, self.coins[i].y, self.coins[i].z) for i in self.faces[indiceFace]]
|
|
pointsFaceOposee = [(self.coins[i].x, self.coins[i].y, self.coins[i].z)
|
|
for i in range(len(self.coins)) if i not in self.faces[indiceFace]]
|
|
|
|
orientation = [0, 0, 0]
|
|
|
|
for i in [0, 1, 2]:
|
|
#on compart les coordonnées du premier point des deux faces, pour x, y puis z
|
|
if pointsFace[0][i] > pointsFaceOposee[0][i]: orientation[i] = 1
|
|
elif pointsFace[0][i] < pointsFaceOposee[0][i]: orientation[i] = -1
|
|
|
|
for point in pointsFace:
|
|
for i in [0, 1, 2]:
|
|
if abs(point[i] - pointsFace[0][i]) > 0.01:
|
|
#if point[i] != pointsFace[0][i]:
|
|
orientation[i] = 0
|
|
# pour donner l'axe de la face,
|
|
#on compart les coordonnées e x,y et z entre les points de la place.
|
|
if orientation == [0, 0, 0]:
|
|
raise Exception("Cette face ne semble pas avoir d'orientation, what?")
|
|
return tuple(orientation)
|
|
|
|
|
|
def affichage(self, screen):
|
|
"""Affiche le cube sur l'écran screen en utilisant l'algorithme du peintre"""
|
|
|
|
t = []
|
|
for coin in self.coins:
|
|
p = coin.projection(self.LARGEUR_ECRAN, self.HAUTEUR_ECRAN, self.ZOOM, self.DISTANCE)
|
|
# Place le point dans une liste de coins transformés
|
|
t.append(p)
|
|
|
|
# Trace la surface en utlisant l'algorithme du peintre
|
|
# Les faces les plus éloignées sont tracées avant les plus proches.
|
|
faces = self.peintre2()
|
|
for f in faces[-3:]:
|
|
pointlist = [(t[f[0]].x, t[f[0]].y), (t[f[1]].x, t[f[1]].y),
|
|
(t[f[1]].x, t[f[1]].y), (t[f[2]].x, t[f[2]].y),
|
|
(t[f[2]].x, t[f[2]].y), (t[f[3]].x, t[f[3]].y),
|
|
(t[f[3]].x, t[f[3]].y), (t[f[0]].x, t[f[0]].y)]
|
|
|
|
pygame.draw.polygon(screen,(0,0,0),pointlist, 3)
|
|
#Pour que les faces caches les traits
|
|
#en arrière, ils faut les affichers après les arrètes
|
|
pygame.draw.polygon(screen,self.couleurs[self.faces.index(f)],pointlist)
|
|
#pygame.display.flip()
|
|
#pygame.time.wait(100)
|
|
|
|
def copie(self):
|
|
"""Retourne une copie de l'objet."""
|
|
newCubie = Cubie3D((0,0,0))
|
|
newCubie.centre = self.centre
|
|
newCubie.coins = self.coins
|
|
newCubie.faces = self.faces
|
|
newCubie.couleurs = self.couleurs
|
|
newCubie.couleursResolution = self.couleursResolution
|
|
return newCubie
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
###nb_image = 0 #######
|
|
|
|
pygame.init()
|
|
|
|
cubes = [Cubie3D((-2,-2,-2))]
|
|
angle = 5
|
|
screen = pygame.display.set_mode((500, 500))
|
|
pygame.display.set_caption("Rubick's Cube")
|
|
|
|
rot = True
|
|
|
|
while True:
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
pygame.quit()
|
|
sys.exit()
|
|
if event.type == pygame.KEYDOWN:
|
|
if event.key == pygame.K_SPACE:
|
|
if rot:
|
|
rot = False
|
|
else:
|
|
rot = True
|
|
if event.key == pygame.K_LEFT:
|
|
angle = 5
|
|
if event.key == pygame.K_RIGHT:
|
|
angle = -5
|
|
screen.fill((255,255,255))
|
|
for cube in cubes:
|
|
cube.affichage(screen)
|
|
#if nb_image == 40:
|
|
# pygame.quit()
|
|
# sys.exit()
|
|
#pygame.image.save(screen,"./img/" + str(nb_image)+".png")
|
|
#nb_image+=1################################################
|
|
pygame.display.flip()
|
|
pygame.time.wait(100)
|
|
if rot:
|
|
for i, cube in enumerate(cubes):
|
|
cubes[i] = cube.rotationZ(angle)#.rotationY(angle).rotationX(angle)
|