3 Points to 3D Angle

#!/usr/bin/python3
# ===================================================================
# given three (3D) points, calculate the (3D) angle formed by them
#
# from: itecnote.com/tecnote/
#       python-code-to-calculate-angle-between-
#       three-point-using-their-3d-coordinates/
# ===================================================================

import sys
import numpy as np
import user_interface as ui

np.seterr(all='raise')


interactive_description = r'''
----------------------------------------------------------------------
In interactive mode, the origin (0,0,0) is asummed to be
the middle point used in the calculation (point B).

            A
           / 
          /
         B
          \ 
           \
            C

Points A are C entered by the user and the angle calculated.

The smaller angle is returned - no angles over 180 degrees is returned

if an error occurs 0.0 is returned
----------------------------------------------------------------------
'''


# --------------------------------------------------------------------
# ---- get 3D angle in degrees from three points a,b,c
# --------------------------------------------------------------------

def get_3d_angle(a, b, c):

    try:
        a = np.array(a)
        b = np.array(b)
        c = np.array(c)

        ba = a - b
        bc = c - b

        cosine_angle = np.dot(ba, bc) / \
                       (np.linalg.norm(ba) * np.linalg.norm(bc))
        ang = np.arccos(cosine_angle)

    except Exception as e:
        ##print(e)
        ang = 0.0

    ang = np.degrees(ang)       # radians to degree
    
    return ang


# -------------------------------------------------------------------
# ---- pre-defined angle calculation tests
# -------------------------------------------------------------------

def predefined_angle_tests():

    tests_3d = [
       [ 'clockwise tests' ],      
       [ (5,0,0),(0,0,0),(5,-5,0),     'a. Test 45   degrees' ],
       [ (5,0,0),(0,0,0),(0,-5,0),     'b. Test 90   degrees' ],
       [ (5,0,0),(0,0,0),(-5,-5,0),    'c. Test 135  degrees' ],
       [ (5,0,0),(0,0,0),(-5,0,0),     'd. Test 180  degrees' ],
       [ (5,0,0),(0,0,0),(-5,5,0),     'e. Test 225  degrees' ],
       [ (5,0,0),(0,0,0),(0,5,0),      'f. Test 270  degrees' ],
       [ (5,0,0),(0,0,0),(5,5,0),      'g. Test 315  degrees' ],
       [ 'special case tests - illegal points/angles ?' ],       
       [ (5,0,0),(0,0,0),(5,0,0),      'x. Test 0    degrees' ], 
       ##[ (0,0,0),(0,0,0),(0,0,0),      'y. Test (0,0,0)' ],
       ##[ (5,5,0),(5,5,0),(5,5,0),      'z. Test (5,5,0)' ],
       [ 'counter clockwise tests' ],       
       [ (5,-5,0), (0,0,0),(5,0,0),    'a. Test -315 degrees' ],
       [ (0,-5,0), (0,0,0),(5,0,0),    'b. Test -270 degrees' ],
       [ (-5,-5,0),(0,0,0),(5,0,0),    'c. Test -225 degrees' ],
       [ (-5,0,0), (0,0,0),(5,0,0),    'd. Test -180 degrees' ],
       [ (-5,5,0), (0,0,0),(5,0,0),    'e. Test -135 degrees' ],
       [ (0,5,0),  (0,0,0),(5,0,0),    'f. Test -90  degrees' ],
       [ (5,5,0),  (0,0,0),(5,0,0),    'g. Test -45  degrees' ]
               ]

    for t  in tests_3d:

        if len(t) == 1:
            print()
            print(t[0])
            print()
            continue

        p0 = np.array(t[0])
        p1 = np.array(t[1])
        p2 = np.array(t[2])

        print(f'----------------------- {t[3]}')
        
        ang = get_3d_angle(p0,p1,p2)
        
        print(f'returned angle   = {ang:<6.2f}  degrees')

# --------------------------------------------------------------------
# ---- ask the user for a point's coordinates
# --------------------------------------------------------------------

def get_point_coordinates(title):

    print()
    print(title)

    while True:

        print()
        s = ui.get_user_input('Enter x,y,z coordinates: ')

        if not s: sys.exit()
        
        s = s.replace(',', ' ')
        s = s.replace(r'/', ' ')
        s = s.replace(':', ' ')
        sx,sy,sz = s.split()
        
        tf,x = ui.is_float(sx)
        if not tf:
            print('bad X coord entered')
            continue

        tf,y = ui.is_float(sy)
        if not tf:
            print('bad Y coord entered')
            continue

        tf,z = ui.is_float(sz)
        if not tf:
            print('bad Z coord entered')
            continue

        return (x,y,z)
    
# --------------------------------------------------------------------
# ---- interactive angle calculation tests
# --------------------------------------------------------------------

def interactive_angle_tests(center_point):

    print(interactive_description)

    while True:

        a = get_point_coordinates(
            'Enter x,y,z coordinates for point A')

        c = get_point_coordinates(
            'Enter x,y,z coordinates for point C')

        ang = get_3d_angle(a,center_point,c)

        print()
        print(f'Angle is {ang:.4} degrees')

        ##ui.pause()

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

if __name__ == '__main__':

    # ---- pre-defined tests
    
    ##predefined_angle_tests()

    # ---- interactive tests
    
    interactive_angle_tests((0,0,0))