Source code for ladybug_geometry.geometry3d.polyface

# coding=utf-8
"""Object with Multiple Planar Faces in 3D Space"""
from __future__ import division

from .pointvector import Vector3D, Point3D
from .ray import Ray3D
from .line import LineSegment3D
from .polyline import Polyline3D
from .plane import Plane
from .face import Face3D
from ._2d import Base2DIn3D

try:
    from itertools import izip as zip  # python 2
except ImportError:
    xrange = range  # python 3


[docs] class Polyface3D(Base2DIn3D): """Object with Multiple Planar Faces in 3D Space. Includes solids and polyhedra. Args: vertices: A list of Point3D objects representing the vertices of this PolyFace. face_indices: A list of lists with one list for each face of the polyface. Each face list must contain at least one tuple of integers corresponding to indices within the vertices list. Additional tuples of integers may follow this one such that the first tuple denotes the boundary of the face while each subsequent tuple denotes a hole in the face. edge_information: Optional edge information, which will speed up the creation of the Polyface object if it is available but should be left as None if it is unknown. If None, edge_information will be computed from the vertices and face_indices inputs. Edge information should be formatted as a dictionary with two keys as follows: * 'edge_indices': An array objects that each contain two integers. These integers correspond to indices within the vertices list and each tuple represents a line segment for an edge of the polyface. * 'edge_types': An array of integers for each edge that parallels the edge_indices list. An integer of 0 denotes a naked edge, an integer of 1 denotes an internal edge. Anything higher is a non-manifold edge. Properties: * vertices * faces * edges * naked_edges * internal_edges * non_manifold_edges * face_indices * edge_indices * edge_types * min * max * center * area * volume * is_solid """ __slots__ = ('_faces', '_edges', '_naked_edges', '_internal_edges', '_non_manifold_edges', '_face_indices', '_edge_indices', '_edge_types', '_area', '_volume', '_is_solid') def __init__(self, vertices, face_indices, edge_information=None): """Initialize Polyface3D.""" # assign input properties Base2DIn3D.__init__(self, vertices) self._face_indices = tuple(tuple(tuple(loop) for loop in face) for face in face_indices) if edge_information is not None: # unpack the input edge information edge_i = edge_information['edge_indices'] edge_t = edge_information['edge_types'] else: # autocalculate the edge information from the vertices and faces edge_i = [] edge_t = [] for face in face_indices: for fi in face: for i, vi in enumerate(fi): try: # this can get slow for large number of vertices ind = edge_i.index((vi, fi[i - 1])) edge_t[ind] += 1 except ValueError: # make sure reversed edge isn't there try: ind = edge_i.index((fi[i - 1], vi)) edge_t[ind] += 1 except ValueError: # add a new edge if fi[i - 1] != vi: # avoid cases of same start and end edge_i.append((fi[i - 1], vi)) edge_t.append(0) self._edge_indices = edge_i if isinstance(edge_i, tuple) else tuple(edge_i) self._edge_types = edge_t if isinstance(edge_t, tuple) else tuple(edge_t) # determine solidity of the polyface by checking for internal edges self._is_solid = True for edge in self._edge_types: if edge != 1: self._is_solid = False break # assign default properties self._faces = None self._edges = None self._naked_edges = None self._internal_edges = None self._non_manifold_edges = None self._area = None self._volume = None
[docs] @classmethod def from_dict(cls, data): """Create a Face3D from a dictionary. Args: data: A python dictionary in the following format .. code-block:: python { "type": "Polyface3D", "vertices": [(0, 0, 0), (10, 0, 0), (10, 10, 0), (0, 10, 0)], "face_indices": [[(0, 1, 2)], [(3, 0, 1)]], "edge_information": { "edge_indices":[(0, 1), (1, 2), (2, 0), (2, 3), (3, 0)], "edge_types":[0, 0, 1, 0, 0] } } """ if 'edge_information' in data and data['edge_information'] is not None: edge_information = data['edge_information'] else: edge_information = None return cls(tuple(Point3D.from_array(pt) for pt in data['vertices']), data['face_indices'], edge_information)
[docs] @classmethod def from_faces(cls, faces, tolerance): """Initialize Polyface3D from a list of Face3D objects. Note that the Polyface3D.faces property of the resulting polyface will have an order of faces that matches the order input to this classmethod. Args: faces: A list of Face3D objects representing the boundary of this Polyface. tolerance: The maximum difference between x, y, and z values at which the vertex of two adjacent faces is considered the same. """ # extract unique vertices from the faces vertices = [] # collection of vertices as point objects face_indices = [] # collection of face indices for f in faces: ind = [] loops = (f.boundary,) if not f.has_holes else (f.boundary,) + f.holes for j, loop in enumerate(loops): ind.append([]) for v in loop: found = False for i, vert in enumerate(vertices): if v.is_equivalent(vert, tolerance): found = True ind[j].append(i) break if not found: # add new point vertices.append(v) ind[j].append(len(vertices) - 1) face_indices.append(tuple(ind)) # get the polyface object and assign correct faces to it face_obj = cls(vertices, face_indices) if face_obj._is_solid: face_obj._faces = cls.get_outward_faces(faces, 0.01) else: face_obj._faces = tuple(faces) return face_obj
[docs] @classmethod def from_box(cls, width, depth, height, base_plane=None): """Initialize Polyface3D from parameters describing a box. Initializing a polyface this way has the added benefit of having its faces property quickly calculated. Args: width: A number for the width of the box (in the X direction). depth: A number for the depth of the box (in the Y direction). height: A number for the height of the box (in the Z direction). base_plane: A Plane object from which to generate the box. If None, default is the WorldXY plane. """ assert isinstance(width, (float, int)), 'Box width must be a number.' assert isinstance(depth, (float, int)), 'Box depth must be a number.' assert isinstance(height, (float, int)), 'Box height must be a number.' if base_plane is not None: assert isinstance(base_plane, Plane), \ 'base_plane must be Plane. Got {}.'.format(type(base_plane)) else: base_plane = Plane(Vector3D(0, 0, 1), Point3D()) _o = base_plane.o _w_vec = base_plane.x * width _d_vec = base_plane.y * depth _h_vec = base_plane.n * height _verts = (_o, _o + _d_vec, _o + _d_vec + _w_vec, _o + _w_vec, _o + _h_vec, _o + _d_vec + _h_vec, _o + _d_vec + _w_vec + _h_vec, _o + _w_vec + _h_vec) _face_indices = ([(0, 1, 2, 3)], [(2, 1, 5, 6)], [(6, 7, 3, 2)], [(0, 3, 7, 4)], [(0, 4, 5, 1)], [(7, 6, 5, 4)]) _edge_indices = ((3, 0), (0, 1), (1, 2), (2, 3), (0, 4), (4, 5), (5, 1), (3, 7), (7, 4), (6, 2), (5, 6), (6, 7)) polyface = cls(_verts, _face_indices, {'edge_indices': _edge_indices, 'edge_types': [1] * 12}) verts = tuple(tuple(_verts[i] for i in face[0]) for face in _face_indices) bottom = Face3D(verts[0], base_plane.flip(), enforce_right_hand=False) middle = tuple(Face3D(v, enforce_right_hand=False) for v in verts[1:5]) top = Face3D(verts[5], base_plane.move(_h_vec), enforce_right_hand=False) polyface._faces = (bottom,) + middle + (top,) polyface._volume = width * depth * height return polyface
[docs] @classmethod def from_offset_face(cls, face, offset): """Initialize a solid Polyface3D from a Face3D offset along its normal. The resulting polyface will always be offset in the direction of the face normal. When a polyface is initialized this way, the first face of the Polyface3D.faces will always be the input face used to create the object, the last face will be the offset version of the face, and all other faces will form the extrusion connecting the two. Args: face: A Face3D to serve as a base for the polyface. offset: A number for the distance to offset the face to make a solid. """ assert isinstance(face, Face3D), \ 'face must be a Face3D. Got {}.'.format(type(face)) assert isinstance(offset, (float, int)), \ 'height must be a number. Got {}.'.format(type(offset)) # compute vertices, face indices, and edges of the extrusion extru_vec = face.normal * offset verts, face_ind_extru, edge_indices = \ Polyface3D._verts_faces_edges_from_boundary(face.boundary, extru_vec) if face.has_holes: _st_i = len(verts) for i, hole in enumerate(face.hole_polygon2d): hole_verts = face._holes[i] if hole.is_clockwise else \ tuple(reversed(face._holes[i])) verts_2, face_ind_extru_2, edge_indices_2 = \ Polyface3D._verts_faces_edges_from_boundary( hole_verts, extru_vec, _st_i) verts.extend(verts_2) face_ind_extru.extend(face_ind_extru_2) edge_indices.extend(edge_indices_2) _st_i += len(hole_verts * 2) face_ind_extru = [[fc] for fc in face_ind_extru] # compute the final faces (accounting for top and bottom) if not face.has_holes: len_faces = len(face.boundary) face_ind_bottom = [tuple(reversed(xrange(len_faces)))] face_ind_top = [tuple( reversed(xrange(len_faces * 2 - 1, len_faces - 1, -1)))] else: face_verts_bottom = [list(reversed(face.boundary))] + list(face.holes) face_verts_top = [[pt.move(extru_vec) for pt in reversed(loop)] for loop in face_verts_bottom] face_ind_bottom = [tuple(verts.index(pt) for pt in loop) for loop in face_verts_bottom] face_ind_top = [tuple(verts.index(pt) for pt in loop) for loop in face_verts_top] faces_ind = [face_ind_bottom] + face_ind_extru + [face_ind_top] # create the polyface and assign known properties. polyface = cls(verts, faces_ind, {'edge_indices': edge_indices, 'edge_types': [1] * len(edge_indices)}) polyface._volume = face.area * offset face_verts = tuple( tuple(tuple(verts[i] for i in loop) for loop in f) for f in faces_ind) if not face.has_holes: polyface._faces = tuple(Face3D(v[0], enforce_right_hand=False) for v in face_verts) else: mid_faces = [Face3D(v[0], enforce_right_hand=False) for v in face_verts[1:-1]] bottom_face = face.flip() top_face = face.move(extru_vec) polyface._faces = tuple([bottom_face] + mid_faces + [top_face]) return polyface
@property def vertices(self): """Tuple of all vertices in this polyface. Note that, in the case of a polyface with holes, some vertices will be repeated since this property effectively traces out a single boundary around the whole shape, winding inward to cut out the holes. """ return self._vertices @property def faces(self): """Tuple of all Face3D objects making up this polyface.""" if self._faces is None: faces = [] for face in self._face_indices: boundary = tuple(self.vertices[i] for i in face[0]) if len(face) == 1: faces.append(Face3D(boundary)) else: holes = tuple(tuple(self.vertices[i] for i in f) for f in face[1:]) faces.append(Face3D(boundary=boundary, holes=holes)) if self._is_solid: self._faces = Polyface3D.get_outward_faces(faces, 0.01) else: self._faces = tuple(faces) return self._faces @property def edges(self): """"Tuple of all edges in this polyface as LineSegment3D objects.""" if self._edges is None: self._edges = tuple(LineSegment3D.from_end_points( self.vertices[seg[0]], self.vertices[seg[1]]) for seg in self._edge_indices) return self._edges @property def naked_edges(self): """"Tuple of all naked edges in this polyface as LineSegment3D objects. Naked edges belong to only one face in the polyface (they are not shared between faces). """ if self._naked_edges is None: self._naked_edges = self._get_edge_type(0) return self._naked_edges @property def internal_edges(self): """"Tuple of all internal edges in this polyface as LineSegment3D objects. Internal edges are shared between two faces in the polyface. """ if self._internal_edges is None: self._internal_edges = self._get_edge_type(1) return self._internal_edges @property def non_manifold_edges(self): """"Tuple of all non-manifold edges in this polyface as LineSegment3D objects. Non-manifold edges are shared between three or more faces and are therefore not allowed in solid polyfaces. """ if self._non_manifold_edges is None: if self._edges is None: self.edges nm_edges = [] for i, type in enumerate(self._edge_types): if type > 1: nm_edges.append(self._edges[i]) self._non_manifold_edges = tuple(nm_edges) return self._non_manifold_edges @property def face_indices(self): """Tuple of face tuples with integers corresponding to indices of vertices.""" return self._face_indices @property def edge_indices(self): """Tuple of edge tuples with integers corresponding to indices of vertices.""" return self._edge_indices @property def edge_types(self): """Tuple of integers for each edge that denotes the type of edge. 0 denotes a naked edge, 1 denotes an internal edge, and anything higher is a non-manifold edge. """ return self._edge_types @property def edge_information(self): """Dictionary with keys edge_indices, edge_types and corresponding properties. """ return {'edge_indices': self._edge_indices, 'edge_types': self._edge_types} @property def area(self): """The total surface area of the polyface.""" if self._area is None: self._area = sum([face.area for face in self.faces]) return self._area @property def volume(self): """The volume enclosed by the polyface. Note that, if this polyface is not solid (with all face normals pointing outward), the value of this property will not be valid. """ if self._volume is None: # formula taken from https://en.wikipedia.org/wiki/Polyhedron#Volume _v = 0 for i, face in enumerate(self.faces): _v += face[0].dot(face.normal) * face.area self._volume = _v / 3 return self._volume @property def is_solid(self): """A boolean to note whether the polyface is solid (True) or is open (False). Note that all solid polyface objects will have faces pointing outwards. """ return self._is_solid
[docs] def merge_overlapping_edges(self, tolerance, angle_tolerance): """Get this object with overlapping naked edges merged into single internal edges This can be used to determine if a polyface is truly solid since this check is not performed by default when the Polyface3D is created from_faces. In the default test of edge conditions, overlapping colinear edges are considered naked when the could be interpreted a single internal edge such as the case below: .. code-block:: shell | 1 | A|______________________|C | B| | | | | | 2 | 3 | If Face 1 only has edge AC and not two separate edges for AB and BC, the creation of the polyface will yield naked edges for AC, AB, and BC, meaning the shape would not be considered solid when it might actually be so. This merge_overlapping_edges method overcomes this by replacing the entire set of 3 naked edges above a single internal edge running from A to C. Args: tolerance: The minimum distance between a vertex and the boundary segments at which point the vertex is considered colinear. angle_tolerance: The max angle in radians that vertices are allowed to differ from one another in order to consider them colinear. """ # get naked edges naked_edges = list(self.naked_edges) if len(naked_edges) == 0: return self # establish lists that will be iteratively edited remove_i = [] add_edges = [] naked_edge_i = [] naked_edge_ind = [] for i, x in enumerate(self.edge_types): if x == 0: naked_edge_i.append(i) naked_edge_ind.append(self._edge_indices[i]) while len(naked_edge_i) > 1: # get all of the edges that are colinear with the first edge coll_edges = list(naked_edge_ind[0]) coll_i = [naked_edge_i[0]] kept_i, maybe_kept_i = [], [] for edge, ind, nei, i in zip( naked_edges[1:], naked_edge_ind[1:], naked_edge_i[1:], xrange(1, len(naked_edges))): try: if edge.is_colinear(naked_edges[0], tolerance, angle_tolerance): coll_edges.extend(ind) coll_i.append(nei) maybe_kept_i.append(i) else: kept_i.append(i) except ZeroDivisionError: # duplicate vertices resulted in 0 length edge coll_edges.extend(ind) coll_i.append(nei) maybe_kept_i.append(i) # determine if colinear edges create a full double line along the edge if len(coll_edges) == 1: # definitely a naked edge overlapping = False else: # all colinear edges are likely a part of the same loop final_vi = [] coll_edges_sort = sorted(coll_edges) overlapping = True for i in range(0, len(coll_edges_sort), 2): final_vi.append(coll_edges_sort[i]) if coll_edges_sort[i] != coll_edges_sort[i + 1]: overlapping = False break # there's still a chance we have multiple colinear loops if not overlapping: dup = {x for x in coll_edges if coll_edges.count(x) > 1} if coll_edges[0] in dup and coll_edges[1] in dup: rebuilt_edges = [(coll_edges[i], coll_edges[i + 1]) for i in range(0, len(coll_edges), 2)] loop_i = set(rebuilt_edges[0]) edge_to_check = rebuilt_edges[1:] more_to_check = True while more_to_check: for i, r_seg in enumerate(edge_to_check): if (r_seg[0] in loop_i or r_seg[1] in loop_i): loop_i.add(r_seg[0]) loop_i.add(r_seg[1]) del edge_to_check[i] break else: more_to_check = False if all(li in dup for li in loop_i): # we have found a loop! final_vi = list(loop_i) new_coll_i = [coll_i[0]] zip_obj = zip(rebuilt_edges[1:], coll_i[1:], maybe_kept_i) for ind, nei, i in zip_obj: if ind[0] in loop_i: new_coll_i.append(nei) else: kept_i.append(i) kept_i.sort() coll_i = new_coll_i overlapping = True # if fully overlapping edges have been found, remake them into one if overlapping: remove_i.extend(coll_i) # remove overlapping edges from the list verts = [self.vertices[j] for j in final_vi] dir_vec = verts[0] - verts[1] if dir_vec.x != 0: vert_coor = [v.x for v in verts] elif dir_vec.y != 0: vert_coor = [v.y for v in verts] else: vert_coor = [v.z for v in verts] vert_coor, final_vi = zip(*sorted(zip(vert_coor, final_vi))) add_edges.append((final_vi[0], final_vi[-1])) # delete the colinear vertices that have been accounted for naked_edges = [naked_edges[i] for i in kept_i] naked_edge_ind = [naked_edge_ind[i] for i in kept_i] naked_edge_i = [naked_edge_i[i] for i in kept_i] # create the new edge information and the new polyface new_edge_indices = list(self.edge_indices) new_edge_types = list(self.edge_types) add_i = [] for i in range(len(new_edge_indices)): if i not in remove_i: add_i.append(i) new_edge_indices = [new_edge_indices[i] for i in add_i] new_edge_types = [new_edge_types[i] for i in add_i] for new_edge in add_edges: new_edge_indices.append(new_edge) new_edge_types.append(1) _new_polyface = Polyface3D( self._vertices, self._face_indices, {'edge_indices': new_edge_indices, 'edge_types': new_edge_types} ) # if the result is still not solid, perform a check on the remaining edges if len(_new_polyface.naked_edges) != 0 and \ len(_new_polyface.non_manifold_edges) == 0: # it's possible that the remaining gaps are smaller than the tolerance small_gaps = True joined_edges = Polyline3D.join_segments(_new_polyface.naked_edges, tolerance) for bnd in joined_edges: if isinstance(bnd, Polyline3D) and bnd.is_closed(tolerance): test_face = Face3D(bnd.vertices) if test_face.check_planar(tolerance, False): min_, max_ = test_face.min, test_face.max max_dim = max(max_.x - min_.x, max_.y - min_.y, max_.z - min_.z) tol_area = tolerance * max_dim if test_face.area > tol_area: small_gaps = False break else: small_gaps = False break else: small_gaps = False break if small_gaps: # the gaps are small enough to make the Polyface closed new_edge_types = [1 for _ in new_edge_types] _new_polyface = Polyface3D( self._vertices, self._face_indices, {'edge_indices': new_edge_indices, 'edge_types': new_edge_types} ) return _new_polyface
[docs] def move(self, moving_vec): """Get a polyface that has been moved along a vector. Args: moving_vec: A Vector3D with the direction and distance to move the polyface. """ _verts = tuple(pt.move(moving_vec) for pt in self.vertices) _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) if self._faces is not None: _new_pface._faces = tuple(face.move(moving_vec) for face in self._faces) _new_pface._volume = self._volume return _new_pface
[docs] def rotate(self, axis, angle, origin): """Rotate a polyface 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. """ _verts = tuple(pt.rotate(axis, angle, origin) for pt in self.vertices) _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) if self._faces is not None: _new_pface._faces = tuple(face.rotate(axis, angle, origin) for face in self._faces) _new_pface._volume = self._volume return _new_pface
[docs] def rotate_xy(self, angle, origin): """Get a polyface rotated counterclockwise in the world XY plane by an angle. Args: angle: An angle in radians. origin: A Point3D for the origin around which the object will be rotated. """ _verts = tuple(pt.rotate_xy(angle, origin) for pt in self.vertices) _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) if self._faces is not None: _new_pface._faces = tuple(face.rotate_xy(angle, origin) for face in self._faces) _new_pface._volume = self._volume return _new_pface
[docs] def reflect(self, normal, origin): """Get a polyface reflected across a plane with the input normal and origin. Args: normal: A Vector3D representing the normal vector for the plane across which the polyface will be reflected. THIS VECTOR MUST BE NORMALIZED. origin: A Point3D representing the origin from which to reflect. """ _verts = tuple(pt.reflect(normal, origin) for pt in self.vertices) _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) if self._faces is not None: _new_pface._faces = tuple(face.reflect(normal, origin) for face in self._faces) _new_pface._volume = self._volume return _new_pface
[docs] def scale(self, factor, origin=None): """Scale a polyface by a factor from an origin point. Args: factor: A number representing how much the polyface 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). """ if origin is None: _verts = tuple(Point3D(pt.x * factor, pt.y * factor, pt.z * factor) for pt in self._vertices) else: _verts = tuple(pt.scale(factor, origin) for pt in self.vertices) _new_pface = Polyface3D(_verts, self.face_indices, self.edge_information) if self._faces is not None: _new_pface._faces = tuple(face.scale(factor, origin) for face in self._faces) _new_pface._volume = self._volume * factor ** 3 \ if self._volume is not None else None return _new_pface
[docs] def is_point_inside(self, point, test_vector=Vector3D(1, 0, 0)): """Test whether a Point3D lies inside or outside the polyface. Note that, if this polyface is not solid, the result will always be False. Args: point: A Point3D for which the inside/outside relationship will be tested. test_vector: Optional vector to set the direction in which intersections with the polyface faces will be evaluated to determine if the point is inside. Default is the X-unit vector. Returns: A boolean denoting whether the point lies inside (True) or outside (False). """ if not self.is_solid: return False test_ray = Ray3D(point, test_vector) n_int = 0 for _f in self.faces: if _f.intersect_line_ray(test_ray): n_int += 1 if n_int % 2 == 0: return False return True
[docs] def does_intersect_line_ray_exist(self, line_ray): """Boolean for whether an intersection exists between the input Line3D or Ray3D. Args: line_ray: A Line3D or Ray3D object for which intersection will be evaluated. Returns: True if an intersection exists. False if it does not exist. """ for face in self.faces: _int = face.intersect_line_ray(line_ray) if _int is not None: return True return False
[docs] def intersect_line_ray(self, line_ray): """Get the intersections between this polyface and the input Line3D or Ray3D. Args: line_ray: A Line3D or Ray3D object for which intersection will be computed. Returns: A list of Point3D for the intersection. Will be an empty list if no intersection exists. """ _inters = [] for face in self.faces: _int = face.intersect_line_ray(line_ray) if _int is not None: _inters.append(_int) return _inters
[docs] def intersect_plane(self, plane): """Get the intersection between this polyface and the input plane. Args: plane: A Plane object for which intersection will be computed. Returns: List of LineSegment3D objects for the intersection. Will be an empty list if no intersection exists. """ _inters = [] for face in self.faces: _int = face.intersect_plane(plane) if _int is not None: _inters.extend(_int) return _inters
[docs] @staticmethod def overlapping_bounding_boxes(polyface1, polyface2, tolerance): """Check if the bounding boxes of two polyfaces overlap within a tolerance. This is particularly useful as a check before performing computationally intense processes between two polyfaces like intersection or checking for adjacency. Checking the overlap of the bounding boxes is extremely quick given this method's use of the Separating Axis Theorem. Args: polyface1: The first polyface to check. polyface2: The second polyface to check. tolerance: Distance within which two points are considered to be co-located. """ # Bounding box check using the Separating Axis Theorem polyf1_width = polyface1.max.x - polyface1.min.x polyf2_width = polyface2.max.x - polyface2.min.x dist_btwn_x = abs(polyface1.center.x - polyface2.center.x) x_gap_btwn_box = dist_btwn_x - (0.5 * polyf1_width) - (0.5 * polyf2_width) if x_gap_btwn_box > tolerance: return False # overlap impossible polyf1_depth = polyface1.max.y - polyface1.min.y polyf2_depth = polyface2.max.y - polyface2.min.y dist_btwn_y = abs(polyface1.center.y - polyface2.center.y) y_gap_btwn_box = dist_btwn_y - (0.5 * polyf1_depth) - (0.5 * polyf2_depth) if y_gap_btwn_box > tolerance: return False # overlap impossible polyf1_height = polyface1.max.z - polyface1.min.z polyf2_height = polyface2.max.z - polyface2.min.z dist_btwn_z = abs(polyface1.center.z - polyface2.center.z) z_gap_btwn_box = dist_btwn_z - (0.5 * polyf1_height) - (0.5 * polyf2_height) if z_gap_btwn_box > tolerance: return False # no overlap return True # overlap exists
[docs] @staticmethod def get_outward_faces(faces, tolerance): """Turn a list of faces forming a solid into one where they all point outward. Note that, if the input faces do not form a closed solid, there may be some output faces that are not pointing outward. However, if the gaps in the combined solid are within the input tolerance, this should not be an issue. Also, note that this method runs automatically for any solid polyface (meaning every solid polyface automatically has outward-facing faces). So there is no need to rerun this method for faces from a solid polyface. Args: faces: A list of Face3D objects that together form a solid. tolerance: Optional tolerance for the permissable size of gap between faces at which point the faces are considered to have a single edge. Returns: outward_faces -- A list of the input Face3D objects that all point outwards (provided the input faces form a solid). """ outward_faces = [] for i, face in enumerate(faces): # construct a ray with the face normal and a point on the face point_on_face = face._point_on_face(tolerance) test_ray = Ray3D(point_on_face, face.normal) # if the ray intersects with an even number of other faces, it is correct n_int = 0 for _f in faces[i + 1:]: if _f.intersect_line_ray(test_ray): n_int += 1 for _f in faces[:i]: if _f.intersect_line_ray(test_ray): n_int += 1 if n_int % 2 == 0: outward_faces.append(face) else: outward_faces.append(face.flip()) return outward_faces
[docs] def to_dict(self, include_edge_information=True): """Get Polyface3D as a dictionary. Args: include_edge_information: Set to True to include the edge_information in the dictionary, which will allow for fast initialization when it is de-serialized. Default True. """ base = {'type': 'Polyface3D', 'vertices': [v.to_array() for v in self.vertices], 'face_indices': self.face_indices} if include_edge_information: base['edge_information'] = self.edge_information return base
def _get_edge_type(self, edge_type): """Get all of the edges of a certain type in this polyface.""" if self._edges is None: self.edges sel_edges = [] for i, type in enumerate(self._edge_types): if type == edge_type: sel_edges.append(self._edges[i]) return tuple(sel_edges) @staticmethod def _verts_faces_edges_from_boundary(cclock_verts, extru_vec, st_i=0): """Get vertices and face indices for a given Face3D loop (boundary or hole).""" verts = list(cclock_verts) + [pt.move(extru_vec) for pt in cclock_verts] len_faces = len(cclock_verts) faces_ind = [] for i in xrange(st_i, st_i + len_faces - 1): faces_ind.append((i, i + 1, i + len_faces + 1, i + len_faces)) faces_ind.append((st_i + len_faces - 1, st_i, st_i + len_faces, st_i + len_faces * 2 - 1)) edge_i1 = [(st_i + i, st_i + i + 1) for i in xrange(len_faces - 1)] edge_i2 = [(st_i + i, st_i + i + len_faces) for i in xrange(len_faces)] edge_i3 = [(st_i + len_faces + i, st_i + len_faces + i + 1) for i in xrange(len_faces - 1)] edge_indices = edge_i1 + [(st_i + len_faces - 1, st_i)] + edge_i2 + edge_i3 + \ [(st_i + len_faces * 2 - 1, st_i + len_faces)] return verts, faces_ind, edge_indices def __copy__(self): _new_poly = Polyface3D(self.vertices, self.face_indices, self.edge_information) _new_poly._faces = self._faces return _new_poly def __key(self): """A tuple based on the object properties, useful for hashing.""" return tuple(hash(pt) for pt in self._vertices) + \ tuple(hash(face) for face in self._face_indices) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, Polyface3D) and self.__key() == other.__key() def __repr__(self): return 'Polyface3D ({} faces) ({} vertices)'.format( len(self.faces), len(self))