# 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)