''' Module for triangle in 3-D space.
.. codeauthor:: Yoshifumi Futaana
:mod:`irfpy.util.triangle` module provides a :class:`Triangle`
to express a triangle in a 3-dimensional space.
Internally, most of the calculation and the returned type is implemented
using :class:`irfpy.util.vector3d.Vector3d` class for :class:`Triangle`.
:class:`NpTriangle` class provides almost same functionality as
:class:`Triangle`, though for performance reason the internal calculation
and interface are mostly used by ``numpy`` module.
The :class:`Triangle` and :class:`NpTriangle` provide basic methods for treating triangle.
Associated functions are also implemented.
- Calculating the distance from a point (:func:`distance`)
- Calculating the foot of perpendicular (:func:`foot_of_perpendicular`)
The precision is in float. Numerical effects (e.g. cancellation of
significant digits), are note treate.
'''
import numpy as np
from irfpy.util import vector3d
[docs]class Triangle:
''' A class for triangle.
Triangle is defined by three points:
point0 -> point1 -> point2 -> point0.
Order of the points is important if one calculate the normal vector.
See also :meth:`normalVector()` method.
pointN should be an index-based array: pointN[0], [1], and [2].
Either :class:`List`, :class:`numpy.ndarray`, or
:class:`irfpy.util.vector3d.Vector3d` is acceptable.
>>> p0 = [1, 2, 3]
>>> p1 = [1, 2, 5]
>>> p2 = [2, 4, 6]
>>> t = Triangle(p0, p1, p2)
>>> p0 = np.array(p0)
>>> p1 = np.array(p1)
>>> p2 = np.array(p2)
>>> t = Triangle(p0, p1, p2)
>>> v0 = vector3d.Vector3d(p0[0], p0[1], p0[2])
>>> v1 = vector3d.Vector3d(p1[0], p1[1], p1[2])
>>> v2 = vector3d.Vector3d(p2[0], p2[1], p2[2])
>>> t = Triangle(v0, v1, v2)
'''
def __init__(self, point0, point1, point2):
# if pointN is just accessible by []
self.p0 = vector3d.Vector3d(point0[0], point0[1], point0[2])
# The stored information is p0, p1-p0, and p2-p0
# for computational time saving for future.
self.p10 = vector3d.Vector3d(point1[0] - point0[0],
point1[1] - point0[1], point1[2] - point0[2])
self.p20 = vector3d.Vector3d(point2[0] - point0[0],
point2[1] - point0[1], point2[2] - point0[2])
[docs] def asNp(self):
''' Return :class:`NpTriangle` instance
>>> p0 = [1, 2, 3]
>>> p1 = [1, 2, 5]
>>> p2 = [2, 4, 6]
>>> t = Triangle(p0, p1, p2)
>>> print(t)
Triangle(Vector3d( 1, 2, 3 ) Vector3d( 1, 2, 5 ) Vector3d( 2, 4, 6 ))
>>> nt = t.asNp()
>>> print(nt) # doctest: +NORMALIZE_WHITESPACE
NpTriangle([1. 2. 3.] [1. 2. 5.] [2. 4. 6.])
'''
p0, p1, p2 = self.getVertices()
return NpTriangle(p0, p1, p2)
[docs] def getArea(self):
''' Return the area.
>>> p0 = [1, 0, 0]
>>> p1 = [0, 1, 0]
>>> p2 = [0, 0, 1]
>>> t = Triangle(p0, p1, p2)
>>> print('%.3f' % t.getArea())
0.866
'''
outer = vector3d.Vector3d()
outer.cross(self.p10, self.p20)
area = outer.length() / 2.
return area
[docs] def normalVector(self):
''' Returns the normal vector.
Returns the normal vector in :class:`irfpy.util.vector3d.Vector3d`
instance. The length is the area.
>>> p0 = [1, 0, 0]
>>> p1 = [0, 1, 0]
>>> p2 = [0, 0, 1]
>>> t = Triangle(p0, p1, p2)
>>> norm = t.normalVector()
>>> print(norm.x, norm.y, norm.z)
0.5 0.5 0.5
>>> t = Triangle(p0, p2, p1)
>>> norm = t.normalVector()
>>> print(norm.x, norm.y, norm.z)
-0.5 -0.5 -0.5
>>> p0 = [1, 0, 0]
>>> p1 = [1, 2, 0]
>>> p2 = [3, 0, 0]
>>> norm = Triangle(p0, p1, p2).normalVector()
>>> print(norm.x, norm.y, norm.z)
0.0 0.0 -2.0
'''
outer = vector3d.Vector3d()
outer.cross(self.p10, self.p20)
outer.scale(0.5)
return outer
[docs] def centerOfGravity(self):
''' Return the center of gravity in Vector3d instance.
>>> p0 = [7, -2, 3]
>>> p1 = [-1, 3, 5]
>>> p2 = [3, -7, 1]
>>> t = Triangle(p0, p1, p2)
>>> v = t.centerOfGravity()
>>> print(v.x)
3.0
>>> print(v.y)
-2.0
>>> print(v.z)
3.0
'''
x = self.p0.x + (self.p10.x + self.p20.x) / 3.0
y = self.p0.y + (self.p10.y + self.p20.y) / 3.0
z = self.p0.z + (self.p10.z + self.p20.z) / 3.0
return vector3d.Vector3d(x, y, z)
[docs] def getVertices(self):
''' Returns three vertices in the specified order in the constructor
>>> p0 = [1, 0, 0]
>>> p1 = [1, 2, 0]
>>> p2 = [3, 0, 0]
>>> v0, v1, v2 = Triangle(p0, p1, p2).getVertices()
>>> print(v0)
Vector3d( 1, 0, 0 )
>>> print(v1)
Vector3d( 1, 2, 0 )
>>> print(v2)
Vector3d( 3, 0, 0 )
'''
p0 = vector3d.Vector3d(self.p0.x, self.p0.y, self.p0.z)
p1 = vector3d.Vector3d(self.p0.x + self.p10.x, self.p0.y + self.p10.y,
self.p0.z + self.p10.z)
p2 = vector3d.Vector3d(self.p0.x + self.p20.x, self.p0.y + self.p20.y,
self.p0.z + self.p20.z)
return (p0, p1, p2)
def __str__(self):
p1 = vector3d.Vector3d()
p1.add(self.p0, self.p10)
str = 'Triangle(%s %s' % (self.p0, p1)
p1.add(self.p0, self.p20)
str = '%s %s)' % (str, p1)
return str
def __repr__(self):
p1 = vector3d.Vector3d()
p1.add(self.p0, self.p10)
str = '<Triangle(%s %s' % (repr(self.p0), repr(p1))
p1.add(self.p0, self.p20)
str = '%s %s>' % (str, repr(p1))
return str
[docs]class NpTriangle:
''' A class for triangle.
Triangle is defined by three points:
point0 -> point1 -> point2 -> point0.
Order of the points is important if one calculate the normal vector.
See also :meth:`normalVector()` method.
pointN should be an index-based array: pointN[0], [1], and [2].
Either iterable, :class:`numpy.ndarray`, or
:class:`irfpy.util.vector3d.Vector3d` is acceptable.
>>> p0 = [1, 2, 3]
>>> p1 = [1, 2, 5]
>>> p2 = [2, 4, 6]
>>> t = NpTriangle(p0, p1, p2)
>>> p0 = np.array(p0)
>>> p1 = np.array(p1)
>>> p2 = np.array(p2)
>>> t = NpTriangle(p0, p1, p2)
>>> v0 = vector3d.Vector3d(p0[0], p0[1], p0[2])
>>> v1 = vector3d.Vector3d(p1[0], p1[1], p1[2])
>>> v2 = vector3d.Vector3d(p2[0], p2[1], p2[2])
>>> t = NpTriangle(v0, v1, v2)
'''
def __init__(self, point0, point1, point2):
# if pointN is just accessible by []
self.p0 = np.array([point0[0], point0[1], point0[2]], dtype=float)
self.p1 = np.array([point1[0], point1[1], point1[2]], dtype=float)
self.p2 = np.array([point2[0], point2[1], point2[2]], dtype=float)
[docs] def getArea(self):
''' Return the area.
>>> p0 = [1, 0, 0]
>>> p1 = [0, 1, 0]
>>> p2 = [0, 0, 1]
>>> t = NpTriangle(p0, p1, p2)
>>> print('%.3f' % t.getArea())
0.866
'''
p10 = self.p1 - self.p0
p20 = self.p2 - self.p0
outer = np.cross(p10, p20)
area = np.linalg.norm(outer) / 2.
return area
[docs] def normalVector(self):
''' Returns the normal vector.
Returns the normal vector in :class:`irfpy.util.vector3d.Vector3d`
instance. The length is the area.
>>> p0 = [1, 0, 0]
>>> p1 = [0, 1, 0]
>>> p2 = [0, 0, 1]
>>> t = NpTriangle(p0, p1, p2)
>>> norm = t.normalVector()
>>> print(norm[0], norm[1], norm[2])
0.5 0.5 0.5
>>> t = NpTriangle(p0, p2, p1)
>>> norm = t.normalVector()
>>> print(norm[0], norm[1], norm[2])
-0.5 -0.5 -0.5
>>> p0 = [1, 0, 0]
>>> p1 = [1, 2, 0]
>>> p2 = [3, 0, 0]
>>> norm = NpTriangle(p0, p1, p2).normalVector()
>>> print(norm[0], norm[1], norm[2])
0.0 0.0 -2.0
'''
p10 = self.p1 - self.p0
p20 = self.p2 - self.p0
outer = np.cross(p10, p20)
return outer * 0.5
[docs] def centerOfGravity(self):
''' Return the center of gravity in Vector3d instance.
>>> p0 = [7, -2, 3]
>>> p1 = [-1, 3, 5]
>>> p2 = [3, -7, 1]
>>> t = NpTriangle(p0, p1, p2)
>>> v = t.centerOfGravity()
>>> print(v[0])
3.0
>>> print(v[1])
-2.0
>>> print(v[2])
3.0
'''
p10 = self.p1 - self.p0
p20 = self.p2 - self.p0
return self.p0 + (p10 + p20) / 3.0
[docs] def getVertices(self):
''' Returns three vertices in the specified order in the constructor
>>> p0 = [1, 0, 0]
>>> p1 = [1, 2, 0]
>>> p2 = [3, 0, 0]
>>> v0, v1, v2 = NpTriangle(p0, p1, p2).getVertices()
>>> print(v0) # doctest: +NORMALIZE_WHITESPACE
[1. 0. 0.]
>>> print(v1) # doctest: +NORMALIZE_WHITESPACE
[1. 2. 0.]
>>> print(v2) # doctest: +NORMALIZE_WHITESPACE
[3. 0. 0.]
'''
return (self.p0, self.p1, self.p2)
def __str__(self):
p0, p1, p2 = self.getVertices()
str = 'NpTriangle(%s %s %s)' % (p0, p1, p2)
return str
def __repr__(self):
return '<%s>' % str(self)
def __getitem__(self, idx):
''' Return the vertex coordinate
>>> p0 = [1, 0, 0]
>>> p1 = [1, 2, 0]
>>> p2 = [3, 0, 0]
>>> t = NpTriangle(p0, p1, p2)
>>> print(t[0]) # doctest: +NORMALIZE_WHITESPACE
[1. 0. 0.]
'''
# return (self.p0, self.p1, self.p2)[idx] # This is relatively slower, slightly...
if idx == 0:
return self.p0
elif idx == 1:
return self.p1
elif idx == 2:
return self.p2
else:
raise IndexError('')
[docs]def distance(tri, point):
''' Returns the distance from a given point to a triangle.
:param tri: Triangle
:type tri: :class:`Triangle`
:param point: Point
:type point: List, ndarray or :class:`Vector3d`
:returns: The distance from point to tri.
This is a general distance which means that a negative value can be
returned if the point is behind the triangle plane.
:rtype: ``float``
Triangle in x-y plane is defined. The normal is ``+z`` diretion.
>>> t = Triangle([1.5, 0, 0], [0, 1.2, 0], [0, 0, 0])
>>> p = [0, 0, 5]
>>> d = distance(t, p)
>>> print('%.2f' % d)
5.00
If the point is behind the triangle plane (i.e. -z area), e.g. (0, 0, -1),
negative value is returned. Absolute value is the distance.
>>> p = [0, 0, -2]
>>> d = distance(t, p)
>>> print('%.2f' % d)
-2.00
It works with :class:`NpTriangle` object.
>>> t = NpTriangle([1.5, 0, 0], [0, 1.2, 0], [0, 0, 0])
>>> p = [0, 0, 5]
>>> d = distance(t, p)
>>> print('%.2f' % d)
5.00
>>> p = [0, 0, -2]
>>> d = distance(t, p)
>>> print('%.2f' % d)
-2.00
'''
if isinstance(tri, Triangle):
norm = tri.normalVector()
norm.normalize()
poin = vector3d.Vector3d(point[0], point[1], point[2])
return norm.dot(poin)
else: # assume numpy version
norm = tri.normalVector()
lennorm = np.linalg.norm(norm)
norm = norm / lennorm
return np.dot(norm, point)
import unittest
import doctest
[docs]def doctests():
return unittest.TestSuite((
doctest.DocTestSuite(),
))
if __name__ == '__main__':
unittest.main(defaultTest='doctests')