#! /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()