Skip to content

Commit

Permalink
feat(polyline3d): Add module and class for Polyline3D
Browse files Browse the repository at this point in the history
This commit adds a class for 3D polylines, complete with methods for intersecting/splitting them with planes and joining segments into Polylines.
  • Loading branch information
chriswmackey authored and Chris Mackey committed Feb 22, 2020
1 parent c9b0ccc commit 7d8f4d2
Show file tree
Hide file tree
Showing 12 changed files with 832 additions and 89 deletions.
82 changes: 82 additions & 0 deletions ladybug_geometry/_polyline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Hidden utility functions used by both Polyline2D and Polyline3D classes."""


def _group_vertices(segments, tolerance):
"""Get lists of joined polyline vertices from segments.
Args:
segments: An array of LineSegment objects.
tolerance: The minimum difference in X, Y, and Z values at which Points
are considred equivalent. Segments with points that match within the
tolerance will be joined.
Returns:
A list of lists vertices that represent joined polylines.
"""
grouped_verts = []
base_seg = segments[0]
remain_segs = list(segments[1:])
while len(remain_segs) > 0:
grouped_verts.append(_build_polyline(base_seg, remain_segs, tolerance))
if len(remain_segs) > 1:
base_seg = remain_segs[0]
del remain_segs[0]
elif len(remain_segs) == 1: # lone last segment
grouped_verts.append([segments[0].p1, segments[0].p2])
del remain_segs[0]
return grouped_verts


def _build_polyline(base_seg, other_segs, tol):
"""Attempt to build a list of polyline vertices from a base segment.
Args:
base_seg: A LineSegment to serve as the base of the Polyline.
other_segs: A list of other LineSegment objects to attempt to
connect to the base_seg. This method will delete any segments
that are successfully connected to the output from this list.
tol: The tolerance to be used for connecting the line.
Returns:
A list of vertices that represent the longest Polyline to which the
base_seg can be a part of given the other_segs as connections.
"""
poly_verts = [base_seg.p1, base_seg.p2]
more_to_check = True
while more_to_check:
for i, r_seg in enumerate(other_segs):
if _connect_seg_to_poly(poly_verts, r_seg, tol):
del other_segs[i]
break
else:
more_to_check = False
return poly_verts


def _connect_seg_to_poly(poly_verts, seg, tol):
"""Connect a LineSegment to a list of polyline vertices.
If successful, a Point will be appended to the poly_verts list and True
will be returned. If not successful, the poly_verts list will remain unchanged
and False will be returned.
Args:
poly_verts: An ordered list of Points to which the segment should
be connected.
seg: A LineSegment to connect to the poly_verts.
tol: The tolerance to be used for connecting the line.
"""
p1, p2 = seg.p1, seg.p2
if poly_verts[-1].is_equivalent(p1, tol):
poly_verts.append(p2)
return True
elif poly_verts[0].is_equivalent(p2, tol):
poly_verts.insert(0, p1)
return True
elif poly_verts[-1].is_equivalent(p2, tol):
poly_verts.append(p1)
return True
elif poly_verts[0].is_equivalent(p1, tol):
poly_verts.insert(0, p2)
return True
return False
8 changes: 4 additions & 4 deletions ladybug_geometry/geometry2d/arc.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def intersect_line_ray(self, line_ray):
return intersect_line2d_arc2d(line_ray, self)

def intersect_line_infinite(self, line_ray):
"""Get the intersection between this Arc2D and an infinitely extending Ray2D .
"""Get the intersection between this Arc2D and an infinitely extending Ray2D.
Args:
line_ray: Another LineSegment2D or Ray2D or to intersect.
Expand All @@ -341,14 +341,14 @@ def split_line_infinite(self, line_ray):
Returns:
A list with 2 or 3 Arc2D objects if the split was successful.
None if no intersection exists.
Will be a list with 1 Arc2D if no intersection exists.
"""
inters = intersect_line2d_infinite_arc2d(line_ray, self)
if inters is None:
return None
return [self]
elif self.is_circle:
if len(inters) != 2:
return None
return [self]
a1 = self._a_from_pt(inters[0])
a2 = self._a_from_pt(inters[1])
return [Arc2D(self.c, self.r, a1, a2), Arc2D(self.c, self.r, a2, a1)]
Expand Down
84 changes: 5 additions & 79 deletions ladybug_geometry/geometry2d/polyline.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
"""2D Polyline"""
from __future__ import division

from ._2d import Base2DIn2D
from .pointvector import Point2D
from .line import LineSegment2D
from .polygon import Polygon2D
from ..intersection2d import intersect_line2d, intersect_line2d_infinite
from ._2d import Base2DIn2D
from .._polyline import _group_vertices


class Polyline2D(Base2DIn2D):
Expand Down Expand Up @@ -57,11 +58,6 @@ def from_dict(cls, data):
interp = data['interpolated'] if 'interpolated' in data else False
return cls(tuple(Point2D.from_array(pt) for pt in data['vertices']), interp)

@property
def vertices(self):
"""Tuple of all vertices in this geometry."""
return self._vertices

@property
def segments(self):
"""Tuple of all line segments in the polyline."""
Expand Down Expand Up @@ -267,19 +263,8 @@ def join_segments(segments, tolerance):
joined segments.
"""
# group the vertices that make up polylines
grouped_verts = []
base_seg = segments[0]
remain_segs = list(segments[1:])
while len(remain_segs) > 0:
grouped_verts.append(
Polyline2D._build_polyline(base_seg, remain_segs, tolerance))
if len(remain_segs) > 1:
base_seg = remain_segs[0]
del remain_segs[0]
elif len(remain_segs) == 1: # lone last segment
grouped_verts.append([segments[0].p1, segments[0].p2])
del remain_segs[0]

grouped_verts = _group_vertices(segments, tolerance)

# create the Polyline2D and LineSegment2D objects
joined_lines = []
for v_list in grouped_verts:
Expand All @@ -290,69 +275,10 @@ def join_segments(segments, tolerance):
return joined_lines

def _transfer_properties(self, new_polyline):
"""Transfer properties from this polyline to a new polyline.
This is used by the transform methods that don't alter the relationship of
face vertices to one another (move, rotate, reflect).
"""
"""Transfer properties from this polyline to a new polyline."""
new_polyline._interpolated = self._interpolated
new_polyline._length = self._length
new_polyline._is_self_intersecting = self._is_self_intersecting

@staticmethod
def _build_polyline(base_seg, other_segs, tol):
"""Attempt to build a list of polyline vertices from a base segment.
Args:
base_seg: A LineSegment2D to serve as the base of the Polyline.
other_segs: A list of other LineSegment2D objects to attempt to
connect to the base_seg. This method will delete any segments
that are successfully connected to the output from this list.
tol: The tolerance to be used for connecting the line.
Returns:
A list of vertices that represent the longest Polyline to which the
base_seg can be a part of given the other_segs as connections.
"""
poly_verts = [base_seg.p1, base_seg.p2]
more_to_check = True
while more_to_check:
for i, r_seg in enumerate(other_segs):
if Polyline2D._connect_seg_to_poly(poly_verts, r_seg, tol):
del other_segs[i]
break
else:
more_to_check = False
return poly_verts

@staticmethod
def _connect_seg_to_poly(poly_verts, seg, tol):
"""Connect a LineSegment2D to a list of polyline vertices.
If successful, a Point2D will be appended to the poly_verts list and True
will be returned. If not successful, the poly_verts list will remain unchanged
and False will be returned.
Args:
poly_verts: An ordered list of Poin2Ds to which the segment should
be connected.
seg: A LineSegment2D to connect to the poly_verts.
tol: The tolerance to be used for connecting the line.
"""
p1, p2 = seg.p1, seg.p2
if poly_verts[-1].is_equivalent(p1, tol):
poly_verts.append(p2)
return True
elif poly_verts[0].is_equivalent(p2, tol):
poly_verts.insert(0, p1)
return True
elif poly_verts[-1].is_equivalent(p2, tol):
poly_verts.append(p1)
return True
elif poly_verts[0].is_equivalent(p1, tol):
poly_verts.insert(0, p2)
return True
return False

def __copy__(self):
return Polyline2D(self._vertices, self._interpolated)
Expand Down
14 changes: 13 additions & 1 deletion ladybug_geometry/geometry3d/_1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from .pointvector import Point3D, Vector3D
from ..intersection3d import closest_point3d_on_line3d, \
closest_point3d_on_line3d_infinite
closest_point3d_on_line3d_infinite, intersect_line3d_plane


class Base1DIn3D(object):
Expand Down Expand Up @@ -113,6 +113,18 @@ def distance_to_point(self, point):
close_pt = self.closest_point(point)
return point.distance_to_point(close_pt)

def intersect_plane(self, plane):
"""Get the intersection between this object and a Plane.
Args:
plane: A Plane that will be intersected with this object.
Returns:
A Point3D object if the intersection was successful.
None if no intersection exists.
"""
return intersect_line3d_plane(self, plane)

def duplicate(self):
"""Get a copy of this object."""
return self.__copy__()
Expand Down
29 changes: 25 additions & 4 deletions ladybug_geometry/geometry3d/arc.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,36 @@ def distance_to_point(self, point):
close_pt = self.closest_point(point)
return point.distance_to_point(close_pt)

def intersect_plane(self, plane):
"""Get the intersection between this Arc3D and a Plane.
Args:
plane: A Plane that will be intersected with this arc.
Returns:
A list of Point3D objects if the intersection was successful.
None if no intersection exists.
"""
_plane_int_ray = plane.intersect_plane(self.plane)
if _plane_int_ray is not None:
_p12d = self.plane.xyz_to_xy(_plane_int_ray.p)
_p22d = self.plane.xyz_to_xy(_plane_int_ray.p + _plane_int_ray.v)
_v2d = _p22d - _p12d
_int_ray2d = Ray2D(_p12d, _v2d)
_int_pt2d = self.arc2d.intersect_line_infinite(_int_ray2d)
if _int_pt2d is not None:
return [self.plane.xy_to_xyz(pt) for pt in _int_pt2d]
return None

def split_with_plane(self, plane):
"""Split this Arc3D in 2 or 3 smaller arcs using a Plane.
Args:
plane: A Plane that will be used to split this arc.
Returns:
A list with two Arc3D objects if the split was successful.
None if no intersection exists.
A list with two or three Arc3D objects if the split was successful.
Will be a list with 1 Arc3D if no intersection exists.
"""
_plane_int_ray = plane.intersect_plane(self.plane)
if _plane_int_ray is not None:
Expand All @@ -297,10 +318,10 @@ def split_with_plane(self, plane):
_v2d = _p22d - _p12d
_int_ray2d = Ray2D(_p12d, _v2d)
_int_pt2d = self.arc2d.split_line_infinite(_int_ray2d)
if _int_pt2d is not None:
if len(_int_pt2d) != 1:
return [Arc3D(self.plane, self.radius, arc.a1, arc.a2)
for arc in _int_pt2d]
return None
return [self]

def duplicate(self):
"""Get a copy of this object."""
Expand Down
16 changes: 16 additions & 0 deletions ladybug_geometry/geometry3d/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,22 @@ def point_at_length(self, length):
"""
return self.p + self.v * (length / self.length)

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]

def to_dict(self):
"""Get LineSegment3D as a dictionary."""
base = Base1DIn3D.to_dict(self)
Expand Down
17 changes: 16 additions & 1 deletion ladybug_geometry/geometry3d/plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,26 @@ def xy_to_xyz(self, point):
"""Get a Point3D from a Point2D in the coordinate system of this plane."""
# This method returns the same result as the following code:
# self.o + (self.x * point.x) + (self.y * point.y)
# It has been wirtten explicitly to cut out the isinstance() calls for speed
# It has been wirtten explicitly to cut out the isinstance() checks for speed
_u = (self.x.x * point.x, self.x.y * point.x, self.x.z * point.x)
_v = (self.y.x * point.y, self.y.y * point.y, self.y.z * point.y)
return Point3D(
self.o.x + _u[0] + _v[0], self.o.y + _u[1] + _v[1], self.o.z + _u[2] + _v[2])

def is_point_above(self, point):
"""Test if a given point is above or below this plane.
Above is defined as being on the side of the plane that the plane normal
is pointing towards.
Args:
point: A Point3D object to test.
Returns:
True is point is above; False if below.
"""
vec = Vector3D(point.x - self.o.x, point.y - self.o.y, point.z - self.o.z)
return self.n.dot(vec) > 0

def closest_point(self, point):
"""Get the closest Point3D on this plane to another Point3D.
Expand Down
Loading

0 comments on commit 7d8f4d2

Please sign in to comment.