Draw Solid Object

#! /usr/bin/python3
# ===================================================================
# draw/rotate a graphics object; a solid or wireframe T
# ===================================================================
# There is something wrong with this code. It does not distinguish
# between which polygon is in front of or behind another polygon.
# It just draws them in the order they appear in the data structure
# (polys).
#
# Z-buffering aka Depth Buffering (Wikipedia)
# draws polygons based on where they fall along the Z axis. It draws the
# farthest away first. This way the nearer polygons are drawn on top of
# the farther ones. This assumes the viewer will always be at +Z infinity.
# Not a bad assumption.
#
# Another way is to calculate a normal vector for each polygon
# and testing to see if it points toward the viewer or away
# from the viewer. For solid objects, polygons with  vectors that
# point away from the viewer can not be seen and do not need to
# be drawn.
# ===================================================================
# When drawing solid objects, it is sometimes useful to color
# surfaces. Colors "white", "black", "red", "green", "blue", "cyan",
# "yellow", and "magenta" will always be available. For a more
# complete list go here.
#
# In graphics.py you can also specify colors using 'color_rgb(r,g,b)'
# anywhere a color can be specified. For example:
#       c = Circle(Point(100,100),25)
#       c.setFill(color_rgb(130,0,130))
# ===================================================================

import coordinate_conversion as cc
import transformation_matrix as tm
from draw_xy_axes import draw_xy_axes
import user_interface as ui
from graphics import *
import numpy as np
from copy import deepcopy
import re, os, platform, sys

win_height = 801               # window height  
win_width  = 801               # window width


# -------------------------------------------------------------------
# ---- graphics object - 3D character T
# -------------------------------------------------------------------
#
#       15------------------------8
#      /|                        /|
#     7-------------------------0 |
#     | |                       | |
#     | 14-------13     10------|-9
#     |/         /|    /|       |/
#     6---------5 |   2---------1
#               | |   | |
#               | |   | |
#               | |   | |
#               | |   | |
#               | |   | |
#               | 12--|11
#               |/    |/
#               4-----3
#     15                           8
#       +-------------------------+
#    7 /|                      0 /|
#     +-------------------------+ |
#     | |14      13     10      | |
#     | +---------+     +-------|-+ 9
#     |/        5/|   2/|       |/
#     +---------+ |   +---------+
#    6          | |   | |        1
#               | |   | |
#               | |   | |
#               | |   | |
#               | |12 | |11
#               | +---|-+
#               |/    |/
#               +-----+
#              4       3
#
#              +Y    -Z
#               |   /
#               |  /
#               | /
#               |/
#  -X ----------+---------- +X
#              /|
#             / |
#            /  |
#           /   |
#         +Z   -Y
#
# ------------------------------------------------------------------

# ---- list of points defining an object
# ---- in a format required by the transformation matrix (x,y,z,1)

pts = [
    (120,40,20,1),    (120,0,20,1),  (20,0,20,1),    (20,-150,20,1),
    (-20,-150,20,1),  (-20,0,20,1),  (-120,0,20,1),  (-120,40,20,1),
    (120,40,-20,1),   (120,0,-20,1), (20,0,-20,1),   (20,-150,-20,1),
    (-20,-150,-20,1), (-20,0,-20,1), (-120,0,-20,1), (-120,40,-20,1)
      ]

pivot = (0,0,0)

# -------------------------------------------------------------------
# ---- several versions of an object are define here (for testing)
# ---- list of polygons (surfaces) that make up an object
# ---- the format for each polygon (surface) is
# ----        ((surface-points), surface-color)
# -------------------------------------------------------------------

# ---- a single polygon (one surface)
#polys = [ ((pts[0],  pts[1],  pts[2],  pts[3],
#            pts[4],  pts[5],  pts[6],  pts[7]),  "blue")
#        ]

# ---- two polygons (front and back surface)
#polys = [ ((pts[0],  pts[1],  pts[2],  pts[3],
#            pts[4],  pts[5],  pts[6],  pts[7]),  "blue"),
#          ((pts[8],  pts[9],  pts[10], pts[11],
#            pts[12], pts[13], pts[14], pts[15]), "green")
#        ]

# ---- several (but not all) surfaces
polys = [ ((pts[0],  pts[1],  pts[2],  pts[3],
            pts[4],  pts[5],  pts[6],  pts[7]),   "blue"),
          ((pts[8],  pts[9],  pts[10], pts[11],
            pts[12], pts[13], pts[14], pts[15]),  "green"),
          ((pts[7],  pts[15], pts[14], pts[6]),   "red"),
          ((pts[6],  pts[5],  pts[13], pts[14]),  "red"),
          ((pts[5],  pts[13], pts[12], pts[4]),   "yellow"),
          ((pts[4],  pts[3],  pts[11], pts[12]),  "black"),
          ((pts[0],  pts[8],  pts[15], pts[7]),   "grey")
        ]


# -------------------------------------------------------------------
# ---- this class is used as a data store for the solid object
# ---- it also accumulates the transformation matrices
# -------------------------------------------------------------------

class SolidObject:

    def __init__(self,pivot,polys,solid=True):
        self.solid     = solid
        self.polys     = polys.deepcopy()  # copy input poly list
        self.x         = pivot[0]      # wire frame x pivot point
        self.y         = pivot[1]      # wire frame y pivot point
        self.z         = pivot[2]      # wire frame z pivot point
        self.mtrx      = np.identity(4)

    def reset(self):
        self.mtrx  = np.identity(4)

    def rotate_around_x_axis(self,degrees):
        m  = tm.get_x_rotation_matrix_3d(degrees)
        mm = m @ self.mtrx
        self.mtrx = mm

    def rotate_around_y_axis(self,degrees):
        m  = tm.get_y_rotation_matrix_3d(degrees)
        mm = m @ self.mtrx
        self.mtrx = mm

    def rotate_around_z_axis(self,degrees):
        m  = tm.get_z_rotation_matrix_3d(degrees)
        mm = m @ self.mtrx
        self.mtrx = mm

    def get_matrix(self):
        return copy.deepcopy(self.mtrx)


# -------------------------------------------------------------------
# ---- Function: create a polygon graphics object
# -------------------------------------------------------------------

def create_polygon(win, mtrx, poly, width=1):

    polypoints = []

    points = poly[0]           # list of points
    color  = poly[1]           #  color

    for pnt in points:

        p = mtrx @ pnt         # polygon point

        x,y = cc.center_to_win_coords(p[0],p[1],win.width,win.height)

        polypoints.append(Point(x,y))

    pobj = Polygon(polypoints)
    pobj.setWidth(width)
    pobj.setOutline(color)
    pobj.setFill(color)
    return pobj


# -------------------------------------------------------------------
# ---- function: draw solid object
# -------------------------------------------------------------------

def draw_solid_object(win, so, polys, winobjs):

    clear_window(winobjs)

    mtrx = so.get_matrix()     # get transformation matrix

    for poly in polys:

        pobj = create_polygon(win, mtrx, poly)

        pobj.draw(win)

        winobjs.append(pobj)


# -------------------------------------------------------------------
# ---- Function: create a line graphics object
# -------------------------------------------------------------------

def create_line(win, mtrx, start, end, c="black", w=2):

    p0 = mtrx @ start          # start of line
    p1 = mtrx @ end            # end   of line

    sx,sy = cc.center_to_win_coords(p0[0],p0[1],win.width,win.height)
    ex,ey = cc.center_to_win_coords(p1[0],p1[1],win.width,win.height)

    lobj = Line(Point(sx,sy),Point(ex,ey))
    lobj.setWidth(w)
    lobj.setFill(c)
    return lobj

# -------------------------------------------------------------------
# ---- draw wireframe using line graphics objects
# -------------------------------------------------------------------

def draw_wireframe(win, so, polys, winobjs):

    mtrx = so.get_matrix()     # get transformation matrix

    for poly in polys:         # for each polygon

        points = poly[0]       # get polygon's point list
        color  = poly[1]       # get polygon's color

        l = len(points) - 1    # index of last point in polygon

        i = 0                  # list index
        j = 1                  # list index

        while(True):           # process the points in the polygon

            if i > l:          # line start point exists?
                break

            if j > l:          # line end point exist?

                lobj = create_line(win, mtrx, points[i],
                                   points[0], c=color)
                lobj.draw(win)
                winobjs.append(lobj)
                break

            lobj = create_line(win, mtrx, points[i],
                               points[j], c=color)
            lobj.draw(win)
            winobjs.append(lobj)

            i += 1
            j += 1

# -------------------------------------------------------------------
# ---- Function: fast (continuous) rotation
# -------------------------------------------------------------------

def continuous_rotation(win, so, polys, winobjs):

    ui.clear_screen()

    while True:

        clear_window(winobjs)

        if so.solid:
            draw_solid_object(win, so, polys, winobjs)
        else:
            draw_wireframe(win, so, polys, winobjs)

        cmd = ui.get_user_input('Enter x, y, z, or 0: ')

        if not cmd:            # empty string
            break

        if cmd[0] =='x':

            so.rotate_around_x_axis(9.0)

        elif cmd[0] == 'y':

            so.rotate_around_y_axis(9.0)

        elif cmd[0] == 'z':

            so.rotate_around_z_axis(9.0)

        elif cmd[0] == '0':

            so.reset()

        else:

            break

# -------------------------------------------------------------------
# ---- Function: erase graphics objects from window
# -------------------------------------------------------------------

def clear_window(objs):

    for o in objs:
        o.undraw()

    objs.clear()

# -------------------------------------------------------------------
# ---- Function: display list of graphics objects
# -------------------------------------------------------------------

def display_objects(objs,title=None):

    print('---------------------------------------------')
    if title:
        print(f'List Title : {title}')
    print(f'List Length: {len(objs)}')
    print(f'List ID    : {id(objs)}')
    for o in objs:
        print(o)
    print('---------------------------------------------')


# -------------------------------------------------------------------
# ---- main
# -------------------------------------------------------------------

# ---- running Python3

if not ui.running_python3:
    print()
    print('Must run Python3 - exit program')
    print()
    sys.exit()

# ---- create window

win = GraphWin("Solid Object", win_width, win_height)
win.setBackground("white")

# ---- draw X,Y coordinate axes

draw_xy_axes(win,True)

# ---- loop forever

so      = SolidObject(pivot,polys)
winobjs = []

while True:

    # ---- ask the user to make a selection

    ui.clear_screen()

    print('--------------------------------------------')
    print('--------------- Solid Object ---------------')
    print('--------------------------------------------')
    print('[qQ] = quit')
    if so.solid:
        print('[tT] = change to draw wireframe')
    else:
        print('[tT] = change to draw solid object')
    print('re   = reset to initial conditions')
    print('dr   = draw object')
    print('cr   = continuous rotation')

    # ---- ask the user to make a selection

    print()
    cmd = ui.get_user_input('Enter command: ')

    if not cmd:                # empty string?
        break

    # ---- quit

    if cmd == 'q'or cmd == 'Q':
        break

    # ---- toggle draw-solid-object flag

    if cmd == 't' or cmd == 'T':
        so.solid = not so.solid
        continue

    # ---- draw object 

    if cmd == 'dr':
        clear_window(winobjs)
        if so.solid:
            draw_solid_object(win, so, polys, winobjs)
        else:
            draw_wireframe(win, so, polys, winobjs)
        continue

    # ---- reset object

    if cmd == 're':
        clear_window(winobjs)
        so.reset()
        continue

    if cmd == 'cw':
        clear_window(winobjs)
        continue

    if cmd == 'cr':
        continuous_rotation(win, so, polys, winobjs)
        continue

    # ---- oops!

    print()
    print(f'Unknown selection ({cmd})')
    ui.pause()

# ---- close window and exit

win.close()