Source code for ladybug_geometry.geometry3d.line

# coding=utf-8
"""3D Line Segment"""
from __future__ import division

from .pointvector import Point3D, Vector3D
from ..intersection3d import closest_point3d_on_line3d_infinite
from ._1d import Base1DIn3D


[docs] class LineSegment3D(Base1DIn3D): """3D line segment object. Args: p: A Point3D representing the first point of the line segment. v: A Vector3D representing the vector to the second point. Properties: * p * v * p1 * p2 * min * max * center * midpoint * endpoints * length * vertices """ __slots__ = () def __init__(self, p, v): """Initialize LineSegment3D.""" Base1DIn3D.__init__(self, p, v)
[docs] @classmethod def from_end_points(cls, p1, p2): """Initialize a line segment from a start point and and end point. Args: p1: A Point3D representing the first point of the line segment. p2: A Point3D representing the second point of the line segment. """ return cls(p1, p2 - p1)
[docs] @classmethod def from_sdl(cls, s, d, length): """Initialize a line segment from a start point, direction, and length. Args: s: A Point3D representing the start point of the line segment. d: A Vector3D representing the direction of the line segment. length: A number representing the length of the line segment. """ return cls(s, d * length / d.magnitude)
[docs] @classmethod def from_array(cls, line_array): """ Create a LineSegment3D from a nested array of two endpoint coordinates. Args: line_array: Nested tuples ((pt1.x, pt1.y, pt.z), (pt2.x, pt2.y, pt.z)), where pt1 and pt2 represent the endpoints of the line segment. """ return LineSegment3D.from_end_points(*tuple(Point3D(*pt) for pt in line_array))
[docs] @classmethod def from_line_segment2d(cls, line2d, z=0): """Initialize a new LineSegment3D from an LineSegment2D and a z value. Args: line2d: A LineSegment2D to be used to generate the LineSegment3D. z: A number for the Z coordinate value of the line. """ base_p = Point3D(line2d.p.x, line2d.p.y, z) base_v = Vector3D(line2d.v.x, line2d.v.y, 0) return cls(base_p, base_v)
@property def p1(self): """First point (same as p).""" return self.p @property def p2(self): """Second point.""" return Point3D(self.p.x + self.v.x, self.p.y + self.v.y, self.p.z + self.v.z) @property def midpoint(self): """Midpoint.""" return self.point_at(0.5) @property def endpoints(self): """Tuple of endpoints """ return (self.p1, self.p2) @property def length(self): """The length of the line segment.""" return self.v.magnitude @property def vertices(self): """Tuple of both vertices in this object.""" return (self.p1, self.p2)
[docs] def is_horizontal(self, tolerance): """Test whether this line segment is horizontal within a certain tolerance. Args: tolerance: The maximum difference between the z values of the start and end coordinates at which the line segment is considered horizontal. """ return abs(self.v.z) <= tolerance
[docs] def is_vertical(self, tolerance): """Test whether this line segment is vertical within a certain tolerance. Args: tolerance: The maximum difference between the x and y values of the start and end coordinates at which the line segment is considered horizontal. """ return abs(self.v.x) <= tolerance and abs(self.v.y) <= tolerance
[docs] def is_colinear(self, line_ray, tolerance, angle_tolerance=None): """Test whether this object is colinear to another LineSegment3D or Ray3D. Args: line_ray: Another LineSegment3D or Ray3D for which co-linearity with this object will be tested. tolerance: The maximum distance between the line_ray and the infinite extension of this object for them to be considered colinear. angle_tolerance: The max angle in radians that the direction between this object and another can vary for them to be considered parallel. If None, the angle tolerance will not be used to evaluate co-linearity and the lines will only be considered colinear if the endpoints of one line are within the tolerance distance of the other line. (Default: None). """ if angle_tolerance is not None and \ not self.is_parallel(line_ray, angle_tolerance): return False _close_pt = closest_point3d_on_line3d_infinite(self.p1, line_ray) if self.p1.distance_to_point(_close_pt) >= tolerance: return False _close_pt = closest_point3d_on_line3d_infinite(self.p2, line_ray) if self.p2.distance_to_point(_close_pt) >= tolerance: return False return True
[docs] def flip(self): """Get a copy of this line segment that is flipped.""" return LineSegment3D(self.p2, self.v.reverse())
[docs] def move(self, moving_vec): """Get a line segment that has been moved along a vector. Args: moving_vec: A Vector3D with the direction and distance to move the ray. """ return LineSegment3D(self.p.move(moving_vec), self.v)
[docs] def rotate(self, axis, angle, origin): """Rotate a line segment by a certain angle around an axis and origin. Right hand rule applies: If axis has a positive orientation, rotation will be clockwise. If axis has a negative orientation, rotation will be counterclockwise. Args: axis: A Vector3D axis representing the axis of rotation. angle: An angle for rotation in radians. origin: A Point3D for the origin around which the object will be rotated. """ return LineSegment3D(self.p.rotate(axis, angle, origin), self.v.rotate(axis, angle))
[docs] def rotate_xy(self, angle, origin): """Get a line segment rotated counterclockwise in the XY plane by a certain angle. Args: angle: An angle in radians. origin: A Point3D for the origin around which the object will be rotated. """ return LineSegment3D(self.p.rotate_xy(angle, origin), self.v.rotate_xy(angle))
[docs] def reflect(self, normal, origin): """Get a line segment reflected across a plane with the input normal vector and origin. Args: normal: A Vector3D representing the normal vector for the plane across which the line segment will be reflected. THIS VECTOR MUST BE NORMALIZED. origin: A Point3D representing the origin from which to reflect. """ return LineSegment3D(self.p.reflect(normal, origin), self.v.reflect(normal))
[docs] def scale(self, factor, origin=None): """Scale a line segment by a factor from an origin point. Args: factor: A number representing how much the line segment should be scaled. origin: A Point3D representing the origin from which to scale. If None, it will be scaled from the World origin (0, 0, 0). """ return LineSegment3D(self.p.scale(factor, origin), self.v * factor)
[docs] def subdivide(self, distances): """Get Point3D values along the line that subdivide it based on input distances. Args: distances: A list of distances along the line at which to subdivide it. This can also be a single number that will be repeated until the end of the line. """ if isinstance(distances, (float, int)): distances = [distances] # this assert prevents the while loop from being infinite assert sum(distances) > 0, 'Segment subdivisions must be greater than 0' line_length = self.length dist = distances[0] index = 0 sub_pts = [self.p] while dist < line_length: sub_pts.append(self.point_at_length(dist)) if index < len(distances) - 1: index += 1 dist += distances[index] sub_pts.append(self.p2) return sub_pts
[docs] def subdivide_evenly(self, number): """Get Point3D values along the line that divide it into evenly-spaced segments. Args: number: Integer for the number of segments into which the line will be divided. """ # this assert prevents the while loop from being infinite assert number > 0, 'Segment subdivisions must be greater than 0' interval = 1 / number parameter = interval sub_pts = [self.p] while parameter <= 1: sub_pts.append(self.point_at(parameter)) parameter += interval if len(sub_pts) != number + 1: # tolerance issue with last point sub_pts.append(self.p2) return sub_pts
[docs] def point_at(self, parameter): """Get a Point3D at a given fraction along the line segment. Args: parameter: The fraction between the start and end point where the desired point lies. For example, 0.5 will yield the midpoint. """ return self.p + self.v * parameter
[docs] def point_at_length(self, length): """Get a Point3D at a given distance along the line segment. Args: length: The distance along the line from the start point where the desired point lies. """ return self.p + self.v * (length / self.length)
[docs] def split_with_plane(self, plane): """Split this LineSegment3D in 2 smaller LineSegment3Ds using a Plane. Args: plane: A Plane that will be used to split this line segment. Returns: A list of two LineSegment3D objects if the split was successful. Will be a list with 1 LineSegment3D if no intersection exists. """ _plane_int = self.intersect_plane(plane) if _plane_int is not None: return [LineSegment3D.from_end_points(self.p1, _plane_int), LineSegment3D.from_end_points(_plane_int, self.p2)] return [self]
[docs] def to_dict(self): """Get LineSegment3D as a dictionary.""" base = Base1DIn3D.to_dict(self) base['type'] = 'LineSegment3D' return base
[docs] def to_array(self): """ A nested list representing the two line endpoint coordinates.""" return (self.p1.to_array(), self.p2.to_array())
def _u_in(self, u): return u >= 0.0 and u <= 1.0 def __abs__(self): return abs(self.v) def __copy__(self): return LineSegment3D(self.p, self.v) def __key(self): """A tuple based on the object properties, useful for hashing.""" return (hash(self.p), hash(self.v)) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, LineSegment3D) and self.__key() == other.__key() def __repr__(self): return 'LineSegment3D (<%.2f, %.2f, %.2f> to <%.2f, %.2f, %.2f>)' % \ (self.p.x, self.p.y, self.p.z, self.p.x + self.v.x, self.p.y + self.v.y, self.p.z + self.v.z)