# -*- coding: utf-8 -*- """ $Id$ Copyright 2008-2010 Lode Leroy Copyright 2010 Lars Kruse <devel@sumpfralle.de> This file is part of PyCAM. PyCAM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. PyCAM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with PyCAM. If not, see <http://www.gnu.org/licenses/>. """ from pycam.Geometry.Point import Point, Vector from pycam.Geometry.Plane import Plane from pycam.Geometry.Line import Line from pycam.Geometry import TransformableContainer, IDGenerator import pycam.Utils.log try: import OpenGL.GL as GL import OpenGL.GLU as GLU import OpenGL.GLUT as GLUT GL_enabled = True except ImportError: GL_enabled = False class Triangle(IDGenerator, TransformableContainer): __slots__ = ["id", "p1", "p2", "p3", "normal", "minx", "maxx", "miny", "maxy", "minz", "maxz", "e1", "e2", "e3", "normal", "center", "radius", "radiussq", "middle"] def __init__(self, p1=None, p2=None, p3=None, n=None): # points are expected to be in ClockWise order super(Triangle, self).__init__() self.p1 = p1 self.p2 = p2 self.p3 = p3 self.normal = n self.reset_cache() def reset_cache(self): self.minx = min(self.p1.x, self.p2.x, self.p3.x) self.miny = min(self.p1.y, self.p2.y, self.p3.y) self.minz = min(self.p1.z, self.p2.z, self.p3.z) self.maxx = max(self.p1.x, self.p2.x, self.p3.x) self.maxy = max(self.p1.y, self.p2.y, self.p3.y) self.maxz = max(self.p1.z, self.p2.z, self.p3.z) self.e1 = Line(self.p1, self.p2) self.e2 = Line(self.p2, self.p3) self.e3 = Line(self.p3, self.p1) # calculate normal, if p1-p2-pe are in clockwise order if self.normal is None: self.normal = self.p3.sub(self.p1).cross(self.p2.sub( \ self.p1)).normalized() if not isinstance(self.normal, Vector): self.normal = self.normal.get_vector() # make sure that the normal has always a unit length self.normal = self.normal.normalized() self.center = self.p1.add(self.p2).add(self.p3).div(3) self.plane = Plane(self.center, self.normal) # calculate circumcircle (resulting in radius and middle) denom = self.p2.sub(self.p1).cross(self.p3.sub(self.p2)).norm self.radius = (self.p2.sub(self.p1).norm \ * self.p3.sub(self.p2).norm * self.p3.sub(self.p1).norm) \ / (2 * denom) self.radiussq = self.radius ** 2 denom2 = 2 * denom * denom alpha = self.p3.sub(self.p2).normsq \ * self.p1.sub(self.p2).dot(self.p1.sub(self.p3)) / denom2 beta = self.p1.sub(self.p3).normsq \ * self.p2.sub(self.p1).dot(self.p2.sub(self.p3)) / denom2 gamma = self.p1.sub(self.p2).normsq \ * self.p3.sub(self.p1).dot(self.p3.sub(self.p2)) / denom2 self.middle = Point( self.p1.x * alpha + self.p2.x * beta + self.p3.x * gamma, self.p1.y * alpha + self.p2.y * beta + self.p3.y * gamma, self.p1.z * alpha + self.p2.z * beta + self.p3.z * gamma) def __repr__(self): return "Triangle%d<%s,%s,%s>" % (self.id, self.p1, self.p2, self.p3) def copy(self): return self.__class__(self.p1.copy(), self.p2.copy(), self.p3.copy(), self.normal.copy()) def next(self): yield self.p1 yield self.p2 yield self.p3 yield self.normal def get_points(self): return (self.p1, self.p2, self.p3) def get_children_count(self): # tree points per triangle return 7 def to_OpenGL(self, color=None, show_directions=False): if not GL_enabled: return if not color is None: GL.glColor4f(*color) GL.glBegin(GL.GL_TRIANGLES) # use normals to improve lighting (contributed by imyrek) normal_t = self.normal GL.glNormal3f(normal_t.x, normal_t.y, normal_t.z) # The triangle's points are in clockwise order, but GL expects # counter-clockwise sorting. GL.glVertex3f(self.p1.x, self.p1.y, self.p1.z) GL.glVertex3f(self.p3.x, self.p3.y, self.p3.z) GL.glVertex3f(self.p2.x, self.p2.y, self.p2.z) GL.glEnd() if show_directions: # display surface normals n = self.normal c = self.center d = 0.5 GL.glBegin(GL.GL_LINES) GL.glVertex3f(c.x, c.y, c.z) GL.glVertex3f(c.x+n.x*d, c.y+n.y*d, c.z+n.z*d) GL.glEnd() if False: # display bounding sphere GL.glPushMatrix() middle = self.middle GL.glTranslate(middle.x, middle.y, middle.z) if not hasattr(self, "_sphere"): self._sphere = GLU.gluNewQuadric() GLU.gluSphere(self._sphere, self.radius, 10, 10) GL.glPopMatrix() if pycam.Utils.log.is_debug(): # draw triangle id on triangle face GL.glPushMatrix() c = self.center GL.glTranslate(c.x, c.y, c.z) p12 = self.p1.add(self.p2).mul(0.5) p3_12 = self.p3.sub(p12).normalized() p2_1 = self.p1.sub(self.p2).normalized() pn = p2_1.cross(p3_12) GL.glMultMatrixf((p2_1.x, p2_1.y, p2_1.z, 0, p3_12.x, p3_12.y, p3_12.z, 0, pn.x, pn.y, pn.z, 0, 0, 0, 0, 1)) n = self.normal.mul(0.01) GL.glTranslatef(n.x, n.y, n.z) maxdim = max((self.maxx - self.minx), (self.maxy - self.miny), (self.maxz - self.minz)) factor = 0.001 GL.glScalef(factor * maxdim, factor * maxdim, factor * maxdim) w = 0 id_string = "%s." % str(self.id) for ch in id_string: w += GLUT.glutStrokeWidth(GLUT.GLUT_STROKE_ROMAN, ord(ch)) GL.glTranslate(-w/2, 0, 0) for ch in id_string: GLUT.glutStrokeCharacter(GLUT.GLUT_STROKE_ROMAN, ord(ch)) GL.glPopMatrix() if False: # draw point id on triangle face c = self.center p12 = self.p1.add(self.p2).mul(0.5) p3_12 = self.p3.sub(p12).normalized() p2_1 = self.p1.sub(self.p2).normalized() pn = p2_1.cross(p3_12) n = self.normal.mul(0.01) for p in (self.p1, self.p2, self.p3): GL.glPushMatrix() pp = p.sub(p.sub(c).mul(0.3)) GL.glTranslate(pp.x, pp.y, pp.z) GL.glMultMatrixf((p2_1.x, p2_1.y, p2_1.z, 0, p3_12.x, p3_12.y, p3_12.z, 0, pn.x, pn.y, pn.z, 0, 0, 0, 0, 1)) GL.glTranslatef(n.x, n.y, n.z) GL.glScalef(0.001, 0.001, 0.001) w = 0 for ch in str(p.id): w += GLUT.glutStrokeWidth(GLUT.GLUT_STROKE_ROMAN, ord(ch)) GL.glTranslate(-w/2, 0, 0) for ch in str(p.id): GLUT.glutStrokeCharacter(GLUT.GLUT_STROKE_ROMAN, ord(ch)) GL.glPopMatrix() def is_point_inside(self, p): # http://www.blackpawn.com/texts/pointinpoly/default.html # Compute vectors v0 = self.p3.sub(self.p1) v1 = self.p2.sub(self.p1) v2 = p.sub(self.p1) # Compute dot products dot00 = v0.dot(v0) dot01 = v0.dot(v1) dot02 = v0.dot(v2) dot11 = v1.dot(v1) dot12 = v1.dot(v2) # Compute barycentric coordinates denom = dot00 * dot11 - dot01 * dot01 if denom == 0: return False invDenom = 1.0 / denom # Originally, "u" and "v" are multiplied with "1/denom". # We don't do this to avoid division by zero (for triangles that are # "almost" invalid). u = (dot11 * dot02 - dot01 * dot12) * invDenom v = (dot00 * dot12 - dot01 * dot02) * invDenom # Check if point is in triangle return (u > 0) and (v > 0) and (u + v < 1) def subdivide(self, depth): sub = [] if depth == 0: sub.append(self) else: p4 = self.p1.add(self.p2).div(2) p5 = self.p2.add(self.p3).div(2) p6 = self.p3.add(self.p1).div(2) sub += Triangle(self.p1, p4, p6).subdivide(depth - 1) sub += Triangle(p6, p5, self.p3).subdivide(depth - 1) sub += Triangle(p6, p4, p5).subdivide(depth - 1) sub += Triangle(p4, self.p2, p5).subdivide(depth - 1) return sub def get_area(self): cross = self.p2.sub(self.p1).cross(self.p3.sub(self.p1)) return cross.norm / 2