Wire Frame With Scale - Cube

#! /usr/bin/python3
# ===================================================================
# test graphics transformations
# translate/rotate/scale wire frame cube 
#
# This program demonstrates a transformation matrix. A transformation
# matrix transforms the coordinates of points defining a wire frame
# graphics object. In this way you can rotate and translate the wire
# frame object.
#
# Multiple transformation matrices can be collected into one
# transformation matrix for use. This is more efficient than
# multiple transforming matrices.
#
# Center (Cartesian) coordinates are used by the program and
# only converted to window coordinates when it is time to draw
# something in the graphics window.
# 
# In modern computers transformations are done in special graphics
# hardware (GPU - Graphics Processing Unit).
#
# ===================================================================

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

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

# -------------------------------------------------------------------
# ---- graphics (wire frame) object
# -------------------------------------------------------------------

# ---- wire frame (cube) corner points
pts = [ (-50,-50,50),  (-50,50,50),  (50,50,50),  (50,-50,50),
        (-50,-50,-50), (-50,50,-50), (50,50,-50), (50,-50,-50) ]

pivot = (0, 0, 0)

# ---- wire frame (cube) lines
lns = [ (pts[0],pts[1]), (pts[1],pts[2]),
        (pts[2],pts[3]), (pts[3],pts[0]),
        (pts[4],pts[5]), (pts[5],pts[6]),
        (pts[6],pts[7]), (pts[7],pts[4]),
        (pts[0],pts[4]), (pts[1],pts[5]),
        (pts[2],pts[6]), (pts[3],pts[7]) ] 

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

class WireFrame:

    # ---- Internal function to convert wire frame lines into lists
    # ---- (arrays of points) used by the transformation matrix.
    # ----   each line has two points (start,end)
    # ----   each point has a three axes values (x,y,z)
    # ---- X,Y,Z are center (Cartesian) coordinates.

    def _lns_to_mx(self,lns):

        mxlns = []

        for l in lns:

            # ---- line points (start,end)
            p0 = l[0]
            p1 = l[1]

            # ---- line points for transformation matrix (start,end)
            xyz0 = np.array([ p0[0], p0[1], p0[2], 1.0 ])
            xyz1 = np.array([ p1[0], p1[1], p1[2], 1.0 ])

            mxlns.append([xyz0,xyz1])

        return mxlns

    def __init__(self,lns,pivot):
        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.lns = lns         # wire frame lines
        self.mtrx  = np.identity(4)
        self.mxlines = self._lns_to_mx(self.lns)

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

    def set_pivot_point(self,x,y,z):
        self.x = x             # wireframe x location
        self.y = y             # wireframe y location
        self.z = z             # wireframe z location

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

    def get_mx_lines(self):
        return self.mxlines

    def get_x(self):           # wire frame x location
        return self.x

    def get_y(self):           # wire frame y location
        return self.y

    def get_z(self):           # wire frame z location
        return self.z

    def get_xyz1(self):        # wire frame [xyz1] location
        return np.array([self.x, self.y, self.z, 1.0])

    def translate_to_location(self,dx,dy,dz):
        self.x += dx           # wire frame x pivot point
        self.y += dy           # wire frame y pivot point
        self.z += dz           # wire frame z pivot point
        m  = tm.get_translation_matrix_3d(dx,dy,dz)
        mm = m @ self.mtrx
        self.mtrx = mm

    def translate_to_origin(self):
        m  = tm.get_translation_matrix_3d(-self.x,-self.y,-self.z)
        mm = m @ self.mtrx
        self.mtrx = mm
        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

    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 scale_xyz(self,sx,sy,sz):
        m  = tm.get_scaling_matrix_3d(sx,sy,sz)
        mm = m @ self.mtrx
        self.mtrx = mm

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

    def display_matrix(self):
        print(self.mtrx)

    def display_mx_lines(self):
        print('-------------------------------------------------')
        for l in self.mxlines:
            p0 = f'[{l[0][0]}, {l[0][1]}, {l[0][2]}]'
            p1 = f'[{l[1][0]}, {l[1][1]}, {l[1][2]}]'
            print(f'ln: {p0}, {p1}')
        print('-------------------------------------------------')

# -------------------------------------------------------------------
# ---- function: draw X,Y axes
# -------------------------------------------------------------------

def draw_xy_axes(win,tickmarks,linewidth=1,linecolor="black"):

    wx  = win.width            # window width
    wy  = win.height           # window height
    wcx = round(wx/2.0)        # window center X
    wcy = round(wy/2.0)        # window center Y

    # ---- X axis

    xl = Line(Point(0,wcy),Point(wx-1,wcy))
    xl.setWidth(linewidth)
    xl.setFill(linecolor)
    xl.draw(win)

    # ---- Y axis

    yl = Line(Point(wcx,0),Point(wcx,wy-1))
    yl.setWidth(linewidth)
    yl.setFill(linecolor)
    yl.draw(win)

    # ---- tick marks?

    if not tickmarks:
        return

    x1 = wcx                   # X axis center
    x2 = wcx                   # X axis center
    y1 = wcy                   # Y axis center
    y2 = wcy                   # Y axis center

    xt1 = wcy - 5              # X axis tick start
    xt2 = wcy + 6              # Y axis tick end
    yt1 = wcx - 5              # X axis tick start
    yt2 = wcx + 6              # Y axis tick end

    for _ in range(7):

        x1 += 50
        t = Line(Point(x1,xt1),Point(x1,xt2))
        t.setWidth(linewidth)
        t.setFill(linecolor)
        t.draw(win)

        x2 -= 50 
        t = Line(Point(x2,xt1),Point(x2,xt2))
        t.setWidth(linewidth)
        t.setFill(linecolor)
        t.draw(win)

        y1 += 50
        t = Line(Point(xt1,y1),Point(xt2,y1))
        t.setWidth(linewidth)
        t.setFill(linecolor)
        t.draw(win)

        y2 -= 50
        t = Line(Point(xt1,y2),Point(xt2,y2))
        t.setWidth(linewidth)
        t.setFill(linecolor)
        t.draw(win)

    return

# -------------------------------------------------------------------
# ---- Function: create a line
# -------------------------------------------------------------------

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

    p0 = mtrx @ ln[0]          # start of line
    p1 = mtrx @ ln[1]          # end   of line

    xy0 = cc.center_to_win_coords(p0[0],p0[1],win.width,win.height)
    xy1 = cc.center_to_win_coords(p1[0],p1[1],win.width,win.height)

    lobj = Line(Point(xy0[0],xy0[1]), Point(xy1[0],xy1[1]))
    lobj.setWidth(w)
    lobj.setFill(c)
    return lobj

# -------------------------------------------------------------------
# ---- Function: draw wire frame lines
# -------------------------------------------------------------------

def draw_wireframe(win, wireframe, mxlines, winlines):

    mtrx = wireframe.get_matrix()

    for ln in mxlines:
        lobj = create_line(win,mtrx,ln)
        lobj.draw(win)
        winlines.append(lobj)

# -------------------------------------------------------------------
# ---- Function: translate (transformation matrix)
# -------------------------------------------------------------------

def translate_wireframe(win, wireframe, command):

    if command == 't0':
        wireframe.translate_to_origin()
        return True

    x = command.split()

    if len(x) != 2:
        print()
        print(f'oops! bad command ({command})')
        return False

    cmd = x[0]

    (tflag,dist) = ui.is_float(x[1])

    if not tflag:
        print()
        print(f'oops! bad command ({command})')
        return False

    if cmd == 'tx':
        wireframe.translate_to_location(dist,0.0,0.0)
    elif cmd == 'ty':
        wireframe.translate_to_location(0.0,dist,0.0)
    elif cmd == 'tz': 
        wireframe.translate_to_location(0.0,0.0,dist)
    else:
        print()
        print(f'oops! bad command ({command})')
        return False

    return True

# -------------------------------------------------------------------
# ---- Function: rotate (transformation matrix)
# -------------------------------------------------------------------

def rotate_wireframe(win, wireframe, command):

    x = command.split()

    if len(x) != 2:
        print()
        print(f'oops! bad command ({command})')
        return False

    cmd = x[0]

    (tflag,deg) = ui.is_float(x[1])

    if not tflag:
        print()
        print(f'oops! bad command ({command})')
        return False

    if cmd == 'rx': 
        wireframe.rotate_around_x_axis(deg)
    elif cmd == 'ry':
        wireframe.rotate_around_y_axis(deg)
    elif cmd == 'rz':
        wireframe.rotate_around_z_axis(deg)
    else:
        print()
        print(f'oops! bad command ({command})')
        return False

    return True

# -------------------------------------------------------------------
# ---- Function: scale (scale matrix)
# -------------------------------------------------------------------

def scale_wireframe(win, wireframe, command):

    x = command.split()

    if len(x) != 4:
        print()
        print(f'oops! bad command ({command})')
        return False

    (tflag,sx) = ui.is_float(x[1])

    if not tflag:
        print()
        print(f'oops! bad command ({command})')
        return False

    (tflag,sy) = ui.is_float(x[2])

    if not tflag:
        print()
        print(f'oops! bad command ({command})')
        return False

    (tflag,sz) = ui.is_float(x[3])

    if not tflag:
        print()
        print(f'oops! bad command ({command})')
        return False

    wireframe.scale_xyz(sx,sy,sz)

    return True

# -------------------------------------------------------------------
# ---- Function: change wire frame pivot point
# -------------------------------------------------------------------

def change_wireframe_pivot_point(win, wireframe, command):

    x = command.split()

    if len(x) != 4:
        print()
        print(f'oops! bad command ({command})')
        return False

    (tflag,px) = ui.is_float(x[1])

    if not tflag:
        print()
        print(f'oops! bad command ({command})')
        return False

    (tflag,py) = ui.is_float(x[2])

    if not tflag:
        print()
        print(f'oops! bad command ({command})')
        return False

    (tflag,pz) = ui.is_float(x[3])

    if not tflag:
        print()
        print(f'oops! bad command ({command})')
        return False

    wireframe.set_pivot_point(px,py,pz)

    return True


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

def clear_window(objs):

    for o in objs:
        o.undraw()


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

# ---- running Python3

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

# ---- create window

win = GraphWin("Wire Frame", win_width, win_height)
win.setBackground("white")

# ---- draw X,Y coordinate axes

draw_xy_axes(win,True)

# ---- loop forever

lastcmd  = ''
wf       = WireFrame(lns,pivot)
mxlines  = wf.get_mx_lines()
winlines = []

while True:

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

    ui.clear_screen()

    print('----------------------------------------------')
    print('------------ Transformation Matrix -----------')
    print('----------------------------------------------')
    print('[qQ] = quit')
    print('re   = reset to initial conditions')
    print('dr   = draw wire frame')
    print('mx   = display transformation matrix')
    print('ii   = display internal information')
    print('mxl  = display matrix lines')
    print('cw   = clear the graphics window (undraw)')
    print()
    print('------- Build Transformation Matrix --------')
    print()
    print('tx   distance  = translate x distance')
    print('ty   distance  = translate y distance')
    print('tz   distance  = translate z distance')
    print('t0             = translate to origin (0,0,0)')
    print('rx   angle     = rotate around X axis')
    print('ry   angle     = rotate around Y axis')
    print('rz   angle     = rotate around Z axis')
    print('pp   x y z     = set/change pivot point')
    print('sxyz sx xy xz  = scale x,y,z')

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

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

    if not s:                  # empty string?
        break

    # ---- display_transformation matrix

    if s == 'mx':
        print('---------------------------------------')
        wf.display_matrix()
        print('---------------------------------------')
        ui.pause()
        continue

    # ---- display internal information

    if s == 'ii':
        print('---------------------------------------')
        wf.display_matrix()
        print('---------------------------------------')
        x = wf.get_x()
        y = wf.get_y()
        z = wf.get_z()
        print(f'location: x={x}, y={y}, z={z}')
        print('---------------------------------------')
        x = pivot[0]
        y = pivot[1]
        z = pivot[2]
        print(f'pivot: x={x}, y={y}, z={z}')
        print('---------------------------------------')
        print(f'Length of winlines: {len(winlines)}')
        print('---------------------------------------')
        ui.pause()
        continue

    # ---- quit

    if s == 'q':
        break

    # ---- draw wire frame

    if s == 'dr':
        clear_window(winlines)
        winlines = []
        draw_wireframe(win,wf,mxlines,winlines)
        continue

    # ---- reset wireframe

    if s == 're':
        clear_window(winlines)
        wf.reset()
        continue

    if s == 'cw':
        clear_window(winlines)
        continue

    # ---- display matrix lines

    if s == 'mxl':
        wf.display_mx_lines()
        ui.pause()
        continue

    # ---- scale wire frame (allow commas and spaces as separators)

    if s[0:2] == 'pp':
        p = s.replace(',',' ')              # convert comma to space
        if not change_wireframe_pivot_point(win,wf,p):
            ui.pause()
        continue

    # ---- scale wire frame (allow commas and spaces as separators)

    if s[0:4] == 'sxyz':
        ss = s.replace(',',' ')             # convert comma to space
        if not scale_wireframe(win,wf,ss):
            ui.pause()
        continue

    # ---- translate

    if s[0] == 't': 
        if not translate_wireframe(win,wf,s):
            ui.pause()
        continue

    # ---- rotate

    if s[0] == 'r':
        if not rotate_wireframe(win,wf,s):
            ui.pause()
        continue

    # ---- oops!

    print()
    print(f'Unknown command ({s})')
    ui.pause()

# ---- close window and exit

win.close()