import colorsys import copy from typing import NamedTuple import imageio # todo fix this import numpy class Colour: r = 0 g = 0 b = 0 a = 0 def __init__(self, rgb): if len(rgb) == 3: self.r = rgb[0] self.g = rgb[1] self.b = rgb[2] elif len(rgb) == 4: self.r = rgb[0] self.g = rgb[1] self.b = rgb[2] self.a = rgb[3] def set_rgb(self, rgb: list[int]): self.r = rgb[0] self.g = rgb[1] self.b = rgb[2] def get_rgb(self): return [self.r, self.g, self.b] def set_hsv(self, hsv): rgb = [x * 256 for x in colorsys.hsv_to_rgb(hsv[0] / 256, hsv[1] / 256, hsv[2] / 256)] self.r = rgb[0] self.g = rgb[1] self.b = rgb[2] def get_hsv(self): hsv = [x * 256 for x in colorsys.hsv_to_rgb(self.r / 256, self.g / 256, self.b / 256)] return [self.r, self.g, self.b] def get_alpha(self): if self.a > 128: return True else: return False class Point2D(NamedTuple): x: float y: float def get(self): return [self.x, self.y] def conv_coord(self, size, scale): return Point2D((self.x * scale) + size[0] / 2, (self.y * scale) + size[1] / 2) class AbstractPoint3D(NamedTuple): x: float y: float z: float class Point3D(NamedTuple): # todo: # check if i have missed anything obvious x: float y: float z: float def xyz(self): return self def translate(self, t_vec): return Point3D(self.x + t_vec.x, self.y + t_vec.y, self.z + t_vec.z) def inv_translate(self, t_vec): return Point3D(self.x - t_vec.x, self.y - t_vec.y, self.z - t_vec.z, ) def snap(self): # sets x y z to the nearest integer return Point3D(round(self.x), round(self.y), round(self.z)) def apply_matrix(self, rot_matrix: numpy.array): # point: numpy.array = numpy.multiply(rot_matrix, numpy.array([self.x, self.y, self.z, 0])) point = rot_matrix.dot(numpy.array([self.x, self.y, self.z, 1])) point = numpy.delete(point, 3) return Point3D._make(point) def get_2d_point(self, map_matrix): # converts a 3d point into a 2d point (replacement for the projection function) # distance = 0 pos = map_matrix.dot(numpy.array([self.x, self.y, self.z])) # convert from homogenous coordinate to cartesian coordinate pos = [pos[0] / pos[2], pos[1] / pos[2]] # try: # x = ((fp_distance * self.x) / (self.y - distance)) # # calculate y pos # y = ((fp_distance * self.z) / (self.y - distance)) # except ZeroDivisionError: # x = 0 # y = 0 return Point2D(pos[0], pos[1]) def to_list(self): return [self.x, self.y, self.z] class Texture: # this class *should* be finished # Y -> X -> Colour pixels: list[list[Colour]] = [] def __init__(self, path: str): self.pixels = [] image = imageio.imread(path) for i, image_y in enumerate(image): self.pixels.append([]) for image_x in image_y: self.pixels[i].append(Colour(image_x)) def get(self): return self.pixels def get_pixel(self, x, y): return self.pixels[y][x] def get_pixel_alpha(self, x, y): return self.pixels[y][x].get_alpha() class Face: # The face class stores a face with N points, a colour, information about edges and faces with nothing else # todo: # make a cleaner way of extracting values points: list[Point3D] colour: Colour has_faces: bool has_edges: bool def __init__(self, points: list[Point3D], colour: Colour, has_edges: bool, has_faces: bool): self.hasEdges = has_edges # whether the main face has edges if type(colour) is list: raise ZeroDivisionError self.colour = colour self.points = points self.hasFaces = has_faces def get_points(self): # returns the positions of points (not rotated, not relative to the player) return self.points def fix_points_behind_camera(self, fp_dis): cut_off = fp_dis + .05 new_points: list[list[Point3D, bool]] = [] invalid_points = 0 # find and mark invalid points for point in self.points: if point.z <= cut_off: new_points.append([point, False]) invalid_points += 1 else: new_points.append([point, True]) if invalid_points == len(self.points): # check if there are no remaining points self.points = [] return valid_points = [] for j, point in enumerate(new_points): if point[1]: valid_points.append(point[0]) else: # check points forward and backwards in list # could just copy and paste the code twice instead of a for loop but it felt wrong. for k in [-1, 1]: if new_points[(j + k) % len(new_points)][1]: front_point = new_points[(j + k) % len(new_points)][0] diff = front_point.inv_translate(point[0]) percentage = (front_point.z - cut_off) / (front_point.z - point[0].z) new_back_point = Point3D(front_point.x - (diff.x * percentage), front_point.y - (diff.y * percentage), cut_off) valid_points.append(new_back_point) self.points = valid_points class TexturedFace(Face): # todo write the ability to get sub faces # todo figure out a way of storing the 'index' of the face (probably in the cube class) texture: Texture # This class inherits from Face, and instead of a base colour it has a texture def set_texture(self, texture: Texture): # sets the texture of the textured Face self.texture = texture def get_sub_faces(self, face_point_dict): # idk self.texture = 0 class Cube: position: Point3D colour: list[Colour] texture: list[Texture] faces: list[Face] # ordered top - north - east - south - west - bottom cubePoints = [Point3D(.5, .5, .5), Point3D(.5, .5, -.5), Point3D(.5, -.5, .5), Point3D(.5, -.5, -.5), Point3D(-.5, .5, .5), Point3D(-.5, .5, -.5), Point3D(-.5, -.5, .5), Point3D(-.5, -.5, -.5)] # same as the dictionary, but no longer used. not sure why it still exists. # cubeSides = [[3, 7, 5, 1], [2, 6, 7, 3], [4, 5, 7, 6], [0, 1, 5, 4], [0, 2, 3, 1], [6, 2, 0, 4]] # hardcoded dict for the relative positions of cube faces cubeFaces: list[list[Point3D]] = [ [cubePoints[3], cubePoints[7], cubePoints[5], cubePoints[1]], # top [cubePoints[2], cubePoints[6], cubePoints[7], cubePoints[3]], # north [cubePoints[4], cubePoints[5], cubePoints[7], cubePoints[6]], # east [cubePoints[0], cubePoints[1], cubePoints[5], cubePoints[4]], # south [cubePoints[0], cubePoints[2], cubePoints[3], cubePoints[1]], # west [cubePoints[6], cubePoints[2], cubePoints[0], cubePoints[4]]] # bottom] def __init__(self, position: Point3D, texture: list[Colour], has_faces, has_edges): self.faces = [] self.position = Point3D(0, 0, 0) # todo # implement texture self.position = position self.colours = texture self.has_faces = has_faces self.has_edges = has_edges # This initialises faces when the cube is placed. Faces are in global coordinates for i in range(6): new_points = 0 self.faces.append(Face([point.translate(self.position) for point in copy.deepcopy(self.cubeFaces[i])], self.colours[i], self.has_edges, self.has_faces)) def get_faces(self): return copy.deepcopy(self.faces) def set_faces(self, visible): self.has_faces = visible return self def set_edges(self, visible): self.has_edges = visible return self