normal_vector.py

#! /usr/bin/python3
# ===================================================================
# calculate a normal vector to a surface
# -------------------------------------------------------------------
# What is a normal vector?
#
# A normal vector is a vector perpendicular to a surface (3D) or
# a line (2D). If the surface or line is curved, it is perpendicular
# at a specific point. If the surface is flat or the line is straight,
# the normal vector is perpendicular everywhere.
#
# So what is a normal vector good for?
#
# In computer graphics, an object can be made of many small surfaces
# (triangles, rectangles, ...). When drawing a solid object, it would
# speed things up to not draw (rendered) hidden surfaces. A normal
# vector to a flat surface can be used to determine if that surface
# is pointed towards the viewer or away from the viewer. For solid
# objects, a surface pointing away from the viewer can not be seen,
# and does not need to be drawn (rendered).
#
# For example, if the viewer is at infinity on the +Z axis, a normal
# vector's +z value means the surface is pointer toward the viewer
# and should be drawn (rendered). A -z value means the surface is
# pointed away from the viewer and will not be drawn (rendered).
# if z = 0, the surface is sideways to the viewer and may or may
# not be seen by the viewer. It depends on the thickness of the
# object the surface represents. In many cases it is not seen (drawn)
# because it is infinitely thin.
#
# How do I order the three points to make them clockwise
# for this function?
#
# Pretend you are outside of a solid object looking down on one of
# its surfaces. Select 3 points in clockwise order and list them
# in order. For example:
#
# Given the following points on the plane of the object's surface ...
#
#     +-------------------------------+
#     |                               |
#     |  p0       p2            p5    |
#     |                               |
#     |       p4            p3        |
#     |                               |
#     |            p6                 |
#     |                               |
#     +-------------------------------+
#
#  Create a clockwise list of 3 points ...
#
#  p0,p3,p6  or  p2,p5,p3  or  p4,p2,p5  or  p3,p6,p4  or  ...
#
# Notes:
#
# 1. Normal vectors are created/calculated so they point away from
#    an object and not into an object. Pick the "right" three points
#    on a surface in clockwise order and use the right hand rule to
#    see which direction the normal vector points.
#
# 2. The easiest way to pick three points is to pick points that are
#    used to define an object's surface. They can be used to
#    calculate a normal vector to the surface.
#
# -------------------------------------------------------------------
# A flat plane (surface) can defined by three points in 3D space
# that are not in a straight line
# -------------------------------------------------------------------
# ---- pt0        a point on the surface
# ---- pt1        a point on the surface
# ---- pt2        a point on the surface
# ---- xyzmax      maximum x,y,x values allowed
# ----            (we only need the direction; not the magnitude)
# -------------------------------------------------------------------
# ---- In this code:
# ---- a. Point parameters (pt0,pt1,pt2) are entered in a
# ----    clockwise order
# ---- b. To calculate the normal vector, vectors to pt0 and pt2 are
# ----    created/calculated from pt1
# ---- c. Using your left hand to show the direction of the normal
# ----    vector. Curl your fingers (clockwise) over the points
# ----    used. Your thumb points in the direction of the normal
# ----    vector (towards or away from the viewer).
# ===================================================================

import numpy as np

# -------------------------------------------------------------------
# ---- function: create a normal vector to a surface
# -------------------------------------------------------------------

def create_normal_vector(pt0,pt1,pt2,xyzmax):

    # ---- to do the math, convert everything from tuples to
    # ---- numpy arrays

    # ---- pt0 - pt1

    vx = pt0[0] - pt1[0]        # vector x
    vy = pt0[1] - pt1[1]        # vector y
    vz = pt0[2] - pt1[2]        # vector z

    v1 = np.array([vx,vy,vz])   # create vector

    # ---- pt2 - pt1

    vx = pt2[0] - pt1[0]        # vector x
    vy = pt2[1] - pt1[1]        # vector y
    vz = pt2[2] - pt1[2]        # vector z

    v2 = np.array([vx,vy,vz])   # create vector

    # ---- create normal vector (cross product)

    xp = np.cross(v1,v2)

    # ---- scale the normal vector coords
    # ---- (direction is all we need; not magnitude)

    max = np.abs(xp[0])
    if np.abs(xp[1]) > max:
        max = np.abs(xp[1])
    elif np.abs(xp[2]) > max:
        max = np.abs(xp[2])

    scale_factor = np.abs(xyzmax) / max

    x = round(xp[0] * scale_factor)
    y = round(xp[1] * scale_factor)
    z = round(xp[2] * scale_factor)

    scaled_xp = np.array([x,y,z])

    ##print(f'scaled xp = {scaled_xp}')
    ##print('--------------------------------------------------')

    return np.array(scaled_xp)

# -------------------------------------------------------------------
# test three points to see if they are collinear
# (within a given tolerance) 
# return True if 'yes', False if 'no'
#
# Note: the tolerance parameters is there to avoid floating
#       point problems.
# ---------- a very mathematical solution ---------------------------
# three points are collinear if the vectors
# v1 = p1 -> p0  and  v2 = p2 -> p0 are parallel.
# ---------- this is the solution we will use -----------------------
# three points lie on the straight line if the area formed by the
# triangle of these three points is zero. So we will check if the
# area formed by the triangle is zero or not.
# -------------------------------------------------------------------
# www.quora.com/How-can-I-find-the-area-of-a-triangle-in-3D-coordinate-geometry
#
# This method doesn’t use the 3D cross product so it’s more general.
# Let’s do it in N dimensions.
#
# coordinates of vertices are (p1,p2,...,pn), (q1,q2,...,qn), and (r1,r2,...rn)
#
#   A =( q1 − p1)**2 + (q2 − p2)**2 + . . . + (qn − pn)**2
#
#   B = (r1 − q1)**2 + (r2 − q2)**2 + . . . + (rn − qn)**2
#
#   C = (p1 − r1)**2 + (p2 − r2)**2 + . . . + (pn − rn)**2
#
# solve for the area S
#
#   16*(S**2) = (4*A*B) − (C−A−B)**2
# -------------------------------------------------------------------

def points_collinear(p,q,r,tolerance = 0.1):

    # ----- Calculation the area of triangle.
        
    A = (q[0]-p[0])**2 + (q[1]-p[1])**2 + (q[2]-p[2])**2
    
    B = (r[0]-q[0])**2 + (r[1]-q[1])**2 + (r[2]-q[2])**2
    
    C = (p[0]-r[0])**2 + (p[1]-r[1])**2 + (p[2]-r[2])**2
    
    area = np.sqrt(((4*A*B) - (C-A-B)**2)/16)
    
    # ---- test the area of the triangle

    ##print(f'p = ({p[0]},{p[1]},{p[2]})')
    ##print(f'q = ({q[0]},{q[1]},{q[2]})')
    ##print(f'r = ({r[0]},{r[1]},{r[2]})')
    ##print(f'collinear area is {area}')
   
    if area < tolerance:
        return True
    
    return False

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

if __name__ == '__main__':

    import user_interface as ui

    def display_normal_vector(p0,p1,p2,xp,verbose=True):
        print()
        if verbose:
            print(f'p0           : {pts[0]}')
            print(f'p1           : {pts[1]}')
            print(f'p2           : {pts[2]}')
            print(f'v1           : {pts[0]},{pts[1]}')
            print(f'v2           : {pts[2]},{pts[1]}')
        print(f'nornal vector: {xp}  (numpy.cross(v1,v2))')

    print()
    print('Test Creating Normal Vectors - Viewer at +Z infinity')


    # ---------------------------------------------------------------

    pts = [ (50,50,0), (50,-50,0), (-50,-50,0), (-50,50,0) ]

    p0 = pts[0]
    p1 = pts[1]
    p2 = pts[2]

    print()
    print('test 1, surface is X,Y plane, Z = 0')

    xp = create_normal_vector(p0,p1,p2,50)

    display_normal_vector(p0,p1,p2,xp)

    print()
    print('test 2, surface is X,Y plane, Z = 0')

    xp = create_normal_vector(p2,p1,p0,50)

    display_normal_vector(p2,p1,p0,xp)


    # ---------------------------------------------------------------

    pts = [ (0,50,50), (0,50,-50), (0,-50,-50), (0,-50,50) ]

    p0 = pts[0]
    p1 = pts[1]
    p2 = pts[2]

    print()
    print('test 3, surface is Y,Z plane, X = 0')

    xp = create_normal_vector(p0,p1,p2,50)

    display_normal_vector(p0,p1,p2,xp)

    print()
    print('test 4, surface is Y,Z plane, X = 0')

    xp = create_normal_vector(p2,p1,p0,50)

    display_normal_vector(p2,p1,p0,xp)


    # ---------------------------------------------------------------

    pts = [ (50,0,50), (-50,0,50), (-50,0,-50), (50,0,50) ]

    p0 = pts[0]
    p1 = pts[1]
    p2 = pts[2]

    print()
    print('test 5, surface is X,Z plane, Y = 0')

    xp = create_normal_vector(p0,p1,p2,50)

    display_normal_vector(p0,p1,p2,xp)

    print()
    print('test 6, surface is X,Z plane, Y = 0')

    xp = create_normal_vector(p2,p1,p0,50)

    display_normal_vector(p2,p1,p0,xp)

    # ---------------------------------------------------------------

    pts = [ (50,0,50), (-50,0,50), (-50,0,-50) ]

    p0 = pts[0]
    p1 = pts[1]
    p2 = pts[2]

    print()
    print('test 7, points collinear?')    

    if points_collinear(p0,p1,p2):
        print('points ARE collinear')
    else:
        print('points ARE NOT collinear')
        
# ---------------------------------------------------------------

    pts = [ (-50,0,0), (0,0,0), (50,0,0) ]

    p0 = pts[0]
    p1 = pts[1]
    p2 = pts[2]

    print()
    print('test 8, points collinear?')    

    if points_collinear(p0,p1,p2):
        print('points ARE collinear')
    else:
        print('points ARE NOT collinear')

# ---------------------------------------------------------------

    pts = [ (50,50,50), (0,0,0), (-50,-50,-50) ]

    p0 = pts[0]
    p1 = pts[1]
    p2 = pts[2]

    print()
    print('test 9, points collinear?')    

    if points_collinear(p0,p1,p2):
        print('points ARE collinear')
    else:
        print('points ARE NOT collinear')

# ---------------------------------------------------------------

    pts = [ (0,0,0), (0,0,0), (0,0,0) ]

    p0 = pts[0]
    p1 = pts[1]
    p2 = pts[2]

    print()
    print('test 10, points collinear?')    

    if points_collinear(p0,p1,p2):
        print('points ARE collinear')
    else:
        print('points ARE NOT collinear')

# ---------------------------------------------------------------

    pts = [ (50,50,50), (50,50,50), (50,50,50) ]

    p0 = pts[0]
    p1 = pts[1]
    p2 = pts[2]

    print()
    print('test 11, points collinear?')    

    if points_collinear(p0,p1,p2):
        print('points ARE collinear')
    else:
        print('points ARE NOT collinear')
        
    ui.pause()