# -*- coding: utf-8 -*- """ $Id$ Copyright 2008-2009 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 import TransformableContainer, IDGenerator from pycam.Geometry.Point import Point from pycam.Geometry.Plane import Plane from pycam.Geometry.utils import epsilon, sqrt # OpenGLTools will be imported later, if necessary #import pycam.Gui.OpenGLTools try: import OpenGL.GL as GL GL_enabled = True except ImportError: GL_enabled = False class Line(IDGenerator, TransformableContainer): __slots__ = ["id", "p1", "p2", "_vector", "_minx", "_maxx", "_miny", "_maxy", "_minz", "_maxz"] def __init__(self, p1, p2): super(Line, self).__init__() self.p1 = p1 self.p2 = p2 self.reset_cache() def copy(self): return self.__class__(self.p1.copy(), self.p2.copy()) @property def vector(self): if self._vector is None: self._vector = self.p2.sub(self.p1) return self._vector @property def dir(self): return self.vector.normalized() @property def len(self): return self.vector.norm @property def minx(self): if self._minx is None: self._minx = min(self.p1.x, self.p2.x) return self._minx @property def maxx(self): if self._maxx is None: self._maxx = max(self.p1.x, self.p2.x) return self._maxx @property def miny(self): if self._miny is None: self._miny = min(self.p1.y, self.p2.y) return self._miny @property def maxy(self): if self._maxy is None: self._maxy = max(self.p1.y, self.p2.y) return self._maxy @property def minz(self): if self._minz is None: self._minz = min(self.p1.z, self.p2.z) return self._minz @property def maxz(self): if self._maxz is None: self._maxz = max(self.p1.z, self.p2.z) return self._maxz def __repr__(self): return "Line<%g,%g,%g>-<%g,%g,%g>" % (self.p1.x, self.p1.y, self.p1.z, self.p2.x, self.p2.y, self.p2.z) def __cmp__(self, other): """ Two lines are equal if both pairs of points are at the same locations. Otherwise the result is based on the comparison of the first and then the second point. """ if self.__class__ == other.__class__: if (self.p1 == other.p1) and (self.p2 == other.p2): return 0 elif self.p1 != other.p1: return cmp(self.p1, other.p1) else: return cmp(self.p2, other.p2) else: return cmp(str(self), str(other)) def next(self): yield self.p1 yield self.p2 def get_children_count(self): # a line always contains two points return 2 def reset_cache(self): self._vector = None self._minx = None self._maxx = None self._miny = None self._maxy = None self._minz = None self._maxz = None def get_points(self): return (self.p1, self.p2) def point_with_length_multiply(self, l): return self.p1.add(self.dir.mul(l*self.len)) def get_length_line(self, length): """ return a line with the same direction and the specified length """ return Line(self.p1, self.p1.add(self.dir.mul(length))) def closest_point(self, p): v = self.dir if v is None: # for zero-length lines return self.p1 l = self.p1.dot(v) - p.dot(v) return self.p1.sub(v.mul(l)) def dist_to_point_sq(self, p): return p.sub(self.closest_point(p)).normsq def dist_to_point(self, p): return sqrt(self.dist_to_point_sq(p)) def is_point_inside(self, p): if (p == self.p1) or (p == self.p2): # these conditions are not covered by the code below return True dir1 = p.sub(self.p1).normalized() dir2 = self.p2.sub(p).normalized() # True if the two parts of the line have the same direction or if the # point is self.p1 or self.p2. return (dir1 == dir2 == self.dir) or (dir1 is None) or (dir2 is None) 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_LINES) GL.glVertex3f(self.p1.x, self.p1.y, self.p1.z) GL.glVertex3f(self.p2.x, self.p2.y, self.p2.z) GL.glEnd() # (optional) draw a cone for visualizing the direction of each line if show_directions and (self.len > 0): # We can't import OpenGLTools in the header - otherwise server # mode without GTK will break. import pycam.Gui.OpenGLTools pycam.Gui.OpenGLTools.draw_direction_cone(self.p1, self.p2) def get_intersection(self, line, infinite_lines=False): """ Get the point of intersection between two lines. Intersections outside the length of these lines are ignored. Returns (None, None) if no valid intersection was found. Otherwise the result is (CollisionPoint, distance). Distance is between 0 and 1. """ x1, x2, x3, x4 = self.p1, self.p2, line.p1, line.p2 a = x2.sub(x1) b = x4.sub(x3) c = x3.sub(x1) # see http://mathworld.wolfram.com/Line-LineIntersection.html (24) try: factor = c.cross(b).dot(a.cross(b)) / a.cross(b).normsq except ZeroDivisionError: # lines are parallel # check if they are _one_ line if a.cross(c).norm != 0: # the lines are parallel with a distance return None, None # the lines are on one straight candidates = [] if self.is_point_inside(x3): candidates.append((x3, c.norm / a.norm)) elif self.is_point_inside(x4): candidates.append((x4, line.p2.sub(self.p1).norm / a.norm)) elif line.is_point_inside(x1): candidates.append((x1, 0)) elif line.is_point_inside(x2): candidates.append((x2, 1)) else: return None, None # return the collision candidate with the lowest distance candidates.sort(key=lambda (cp, dist): dist) return candidates[0] if infinite_lines or (-epsilon <= factor <= 1 + epsilon): intersection = x1.add(a.mul(factor)) # check if the intersection is between x3 and x4 if infinite_lines: return intersection, factor elif (min(x3.x, x4.x) - epsilon <= intersection.x <= max(x3.x, x4.x) + epsilon) \ and (min(x3.y, x4.y) - epsilon <= intersection.y <= max(x3.y, x4.y) + epsilon) \ and (min(x3.z, x4.z) - epsilon <= intersection.z <= max(x3.z, x4.z) + epsilon): return intersection, factor else: # intersection outside of the length of line(x3, x4) return None, None else: # intersection outside of the length of line(x1, x2) return None, None def get_cropped_line(self, minx, maxx, miny, maxy, minz, maxz): if self.is_completely_inside(minx, maxx, miny, maxy, minz, maxz): return self elif self.is_completely_outside(minx, maxx, miny, maxy, minz, maxz): return None else: # the line needs to be cropped # generate the six planes of the cube for possible intersections minp = Point(minx, miny, minz) maxp = Point(maxx, maxy, maxz) planes = [ Plane(minp, Point(1, 0, 0)), Plane(minp, Point(0, 1, 0)), Plane(minp, Point(0, 0, 1)), Plane(maxp, Point(1, 0, 0)), Plane(maxp, Point(0, 1, 0)), Plane(maxp, Point(0, 0, 1)), ] # calculate all intersections intersections = [plane.intersect_point(self.dir, self.p1) for plane in planes] # remove all intersections outside the box and outside the line valid_intersections = [(cp, dist) for cp, dist in intersections if cp and (-epsilon <= dist <= self.len + epsilon) and \ cp.is_inside(minx, maxx, miny, maxy, minz, maxz)] # sort the intersections according to their distance to self.p1 valid_intersections.sort( cmp=lambda (cp1, l1), (cp2, l2): cmp(l1, l2)) # Check if p1 is within the box - otherwise use the closest # intersection. The check for "valid_intersections" is necessary # to prevent an IndexError due to floating point inaccuracies. if self.p1.is_inside(minx, maxx, miny, maxy, minz, maxz) \ or not valid_intersections: new_p1 = self.p1 else: new_p1 = valid_intersections[0][0] # Check if p2 is within the box - otherwise use the intersection # most distant from p1. if self.p2.is_inside(minx, maxx, miny, maxy, minz, maxz) \ or not valid_intersections: new_p2 = self.p2 else: new_p2 = valid_intersections[-1][0] if new_p1 == new_p2: # no real line return None else: return Line(new_p1, new_p2)