3D Rotation

#!/usr/bin/python3
# ===============================================================
# manipulate a point by rotating it around the X,Y,Z axes
# using transformation matrices
#
# rotation about the XYZ axes can be somewhat weird.
# With 3D rotations I am never sure where things will end up.
#
# There are modification that could be done to consolidate this
# code to make it less verbose. I "opened up" this code to make
# it easier (I hope) to follow what it is doing. 
# ===============================================================

import sys
import math as m
import numpy as np
import graphics as gr
import draw_xy_axes as ax
import user_interface as ui
import coordinate_conversion as cc
import transformation_matrix as tm

menu1 = '''
============================================================
This program allows a user to manipulate a point
(small circle) displayed in graphics window using
transformation matrices to rotate it around the X,Y,Z axes

The graphics window is 801x801 pixels

+ rotation from the x axis is counter clockwise when
viewed from +z infinity.

+ rotation from the y axis is counter clockwise when
viewed from +x infinity.

+ rotation from the z axis is counter clockwise when
viewed from +y infinity.
============================================================
'''

menu2 = '''
option  description
======  ====================================================
  0     reset to initial coordinates (x=300.0,y=300.0,z=0.0) 
  1     rotate around the X axis
  2     rotate around the Y axis
  3     rotate around the Z axis
  7     display current coordinates
  8     change current XYZ coordinates
  9     exit program
'''

# ---------------------------------------------------------------
# rotate XYZ coordinates around the X axis
# return the new XYZ coordinates
# ---------------------------------------------------------------

def rotate_around_x_axis(x,y,z,deg):

    C = np.array([x,y,z,1]) # current point coordinate array 
    
    R = tm.get_x_rotation_matrix_3d(deg)
    
    NC = R@C                # new point coordinates array

    print(f'X axis rotation {deg} degrees')    
    print(f'old coords: x={C[0]:.4f}, y={C[1]:.4f}, z={C[2]:.4f}')
    print(f'new coords: x={NC[0]:.4f}, y={NC[1]:.4f}, z={NC[2]:.4f}')
    
    return (NC[0],NC[1],NC[2])

# ---------------------------------------------------------------
# rotate XYZ coordinates around the Y axis
# return the new XYZ coordinates
# ---------------------------------------------------------------

def rotate_around_y_axis(x,y,z,deg):

    C = np.array([x,y,z,1]) # current point coordinate array 
    
    R = tm.get_y_rotation_matrix_3d(deg)
    
    NC = R@C                # new point coordinates array

    print(f'Y axis rotation {deg} degrees')    
    print(f'old coords: x={C[0]:.4f}, y={C[1]:.4f}, z={C[2]:.4f}')
    print(f'new coords: x={NC[0]:.4f}, y={NC[1]:.4f}, z={NC[2]:.4f}')
    
    return (NC[0],NC[1],NC[2])

# ---------------------------------------------------------------
# rotate XYZ coordinates around the Zaxis
# return the new XYZ coordinates
# ---------------------------------------------------------------

def rotate_around_z_axis(x,y,z,deg):

    C = np.array([x,y,z,1]) # current point coordinate array 
    
    R = tm.get_z_rotation_matrix_3d(deg)
    
    NC = C@R                # new point coordinates array
    
    print(f'Z axis rotation {deg} degrees')    
    print(f'old coords: x={C[0]:.4f}, y={C[1]:.4f}, z={C[2]:.4f}')
    print(f'new coords: x={NC[0]:.4f}, y={NC[1]:.4f}, z={NC[2]:.4f}')
    
    return (NC[0],NC[1],NC[2])

# ---------------------------------------------------------------
# ---- draw a point (small circle) using XYZ coordinates
# ----
# ---- Note: only two of the axes are used to define the plane of
# ---- the graphics window. the third axis defines the viewers
# ---- location (towards the origin (0,0) or away from
# ---- the origin) this code defines the viewer to be located
# ---- +z infinity and the other two (x and y) define the plane
# ---- of the graphics window. this a standard configuration
# ---- (but not the only one).
# ---------------------------------------------------------------

def draw_point(win,x,y,z,color='red',size=4):

    rx = round(x)               # convert to integer
    ry = round(y)               # convert to integer
    rz = round(z)               # convert to integer

    wx,wy = cc.center_to_win_coords(rx,ry,win.width,win.height)
    
    p = gr.Circle(gr.Point(wx,wy),size)
    p.setFill(color)
    p.setOutline('black')
    p.setWidth(1)
    p.draw(win)
    return p

# ---------------------------------------------------------------
# ---- un-draw graphics objects
# ---------------------------------------------------------------

def undraw_graphics_objects(objlst):
    for o in objlst:
        o.undraw()

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

if __name__ == '__main__':

    # ---- create a graphics window

    window_title = 'Rotation Using Transformation Matrices'

    win = gr.GraphWin(window_title,801,801)
    win.setBackground('white')

    # ---- draw X,Y axes with the origin (0,0) in the
    # ---- center of the graphics window

    ax.draw_xy_axes(win)

    # ---- starting conditions

    graphics_objs = []
    initial_x     = 300.0
    initial_y     = 300.0
    initial_z     = 0.0
    current_x     = initial_x
    current_y     = initial_y
    current_z     = initial_z

    p = draw_point(win,initial_x,initial_y,initial_z)
    graphics_objs.append(p)

    print(menu1)

    # ---- menu and menu actions
    
    while True:

        print(menu2)

        # ---- get user's option selection ----------------------

        opt = ui.get_user_input('Enter option: ')
        
        if not opt: break

        opt = opt[0].lower()

        # ---- reset --------------------------------------------

        if opt == 0 or opt == 'r':
            current_x = initial_x
            current_y = initial_y
            current_z = initial_z
            undraw_graphics_objects(graphics_objs)
            graphics_objs = []
            p = draw_point(win,initial_x,initial_y,initial_z)
            graphics_objs.append(p)
            continue

        # ---- rotate around the X axis -------------------------

        if opt == '1' or opt == 'x':

            print()
            prompt = 'Enter rotation around X axis (degrees): '
            sdeg = ui.get_user_input(prompt)
            if not sdeg: continue       # empty string

            tf,deg = ui.is_float(sdeg)
            
            if not tf:
                print()
                print(f'Error: illegal X rotation angle)')
                continue

            # ---- rotate around X axis
             
            x,y,z = rotate_around_x_axis(current_x,current_y,
                                         current_z,deg)

            current_x = x
            current_y = y
            current_z = z

            # ---- draw new point

            undraw_graphics_objects(graphics_objs)
            graphics_objs = []          
            p = draw_point(win,current_x,current_y,current_z)
            graphics_objs.append(p)

            continue

        # ---- rotate around the Y axis -------------------------

        if opt == '2' or opt == 'y':

            print()
            prompt = 'Enter rotation around Y axis (degrees): '
            sdeg = ui.get_user_input(prompt)
            if not sdeg: continue       # empty string

            tf,deg = ui.is_float(sdeg)
            
            if not tf:
                print()
                print(f'Error: illegal X axis rotation angle)')
                continue
            
           # ---- rotate around y axis
             
            x,y,z = rotate_around_y_axis(current_x,current_y,
                                         current_z,deg)

            current_x = x
            current_y = y
            current_z = z

            # ---- draw new point

            undraw_graphics_objects(graphics_objs)
            graphics_objs = []          
            p = draw_point(win,current_x,current_y,current_z)
            graphics_objs.append(p)

            continue

        # ---- rotate around the Z axis -------------------------

        if opt == '3' or opt == 'z':
            print()
            prompt = 'Enter rotation around Z axis (degrees): '
            sdeg = ui.get_user_input(prompt)
            if not sdeg: continue       # empty string

            tf,deg = ui.is_float(sdeg)
            
            if not tf:
                print()
                print(f'Error: illegal Z axis rotation angle)')
                continue
            
           # ---- rotate around Z axis
             
            x,y,z = rotate_around_z_axis(current_x,current_y,
                                         current_z,deg)

            current_x = x
            current_y = y
            current_z = z

            # ---- draw new point

            undraw_graphics_objects(graphics_objs)
            graphics_objs = []          
            p = draw_point(win,current_x,current_y,current_z)
            graphics_objs.append(p)
            
            continue

        # ---- display the current coordinates ------------------

        if opt == '7' or opt == 'd':
            print()
            print(f'current coordinates: X={current_x:.4f}, ' +\
                  f'Y={current_y:.4f}, Z={current_z:.4f}')
            continue
        
        # ---- change initial XYZ coordinates -------------------

        if opt == '8' or opt == 'c':
            print()
            prompt = 'Enter XYZ coordinates: '
            s = ui.get_user_input(prompt)
            s = s.replace(',', ' ')
            s = s.replace(r'/', ' ')
            s = s.replace(':', ' ')
            lst = s.split()
            if len(lst) != 3:
                print()
                print(f'Error: 3 XYZ coordinates required ({s})')

            tf,x = ui.is_float(lst[0])
            if not tf:
                print()
                print(f'Error: illegal X coordinate ({x})')
                continue
            
            tf,y = ui.is_float(lst[1])
            if not tf:
                print()
                print(f'Error: illegal Y coordinate ({y})')
                continue
            
            tf,z = ui.is_float(lst[2])
            if not tf:
                print()
                print(f'Error: illegal Z coordinate ({z})')
                continue

            current_x = x
            current_y = y
            current_z = z
            undraw_graphics_objects(graphics_objs)
            graphics_objs = []
            p = draw_point(win,current_x,current_y,current_y)
            graphics_objs.append(p)
            continue

        # ---- exit program -------------------------------------

        if opt == '9' or opt == 'e':
            break

        # ---- option error -------------------------------------

        print()
        print(f'unknown option ({opt})')
        continue

    win.close()