Source code for irfpy.util.triangle

''' 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)
[docs]def foot_of_perpendicular(tri, point): ''' Returns the position of the foot of perpendicular. :param tri: Triangle :type tri: :class:`Triangle` :param point: Point :type point: List, ndarray or :class:`Vector3d` :returns: The foot-of-perpendicular for the given ``point`` to the given ``tri``. Foot of perpendicular of the point *P* to the triangle *ABC* can be calculate as follows. Write **n** as a normal vector of *ABC* and *d* as a general distance of between *P* and *ABC*. The vector from *H*, the foot of perpendicular, to the point *P* can be written as *d* **n**. Thus, *H* can be written as *P* - *d* **n**. >>> t = Triangle([1.5, 0, 0], [0, 1.2, 0], [0, 0, 0]) >>> p = [3, 2, 5] >>> h = foot_of_perpendicular(t, p) >>> print('%.2f %.2f %.2f' % (h.x, h.y, h.z)) 3.00 2.00 0.00 >>> p = [-3, 4, -5] >>> h = foot_of_perpendicular(t, p) >>> print('%.2f %.2f %.2f' % (h.x, h.y, h.z)) -3.00 4.00 0.00 It works with :class:`NpTriangle` object. >>> t = NpTriangle([1.5, 0, 0], [0, 1.2, 0], [0, 0, 0]) >>> p = [3, 2, 5] >>> h = foot_of_perpendicular(t, p) >>> print('%.2f %.2f %.2f' % (h[0], h[1], h[2])) 3.00 2.00 0.00 >>> p = [-3, 4, -5] >>> h = foot_of_perpendicular(t, p) >>> print('%.2f %.2f %.2f' % (h[0], h[1], h[2])) -3.00 4.00 0.00 ''' if isinstance(tri, Triangle): norm = tri.normalVector() norm.normalize() # Normalized normal vector norm.scale(-distance(tri, point)) p = vector3d.Vector3d(point[0], point[1], point[2]) p.add(norm) return p else: norm = tri.normalVector() lennorm = np.linalg.norm(norm) norm = norm / lennorm norm = norm * -distance(tri, point) return point + norm
import unittest import doctest
[docs]def doctests(): return unittest.TestSuite(( doctest.DocTestSuite(), ))
if __name__ == '__main__': unittest.main(defaultTest='doctests')