Source code for ladybug_rhino.intersect

"""Functions to handle intersection of Rhino geometries.

These represent geometry computation methods  that are either not supported by
ladybug_geometry or there are much more efficient versions of them in Rhino.
"""
import math
import operator
import array as specializedarray

try:
    import System.Threading.Tasks as tasks
    from System import Array
    import clr
except ImportError as e:
    print('Failed to import Windows/.NET libraries\nParallel processing functionality '
          'will not be available\n{}'.format(e))

try:
    import Rhino.Geometry as rg
except ImportError as e:
    raise ImportError("Failed to import Rhino.\n{}".format(e))

from .config import tolerance, rhino_version


[docs] def join_geometry_to_mesh(geometry): """Convert an array of Rhino Breps and/or Meshes into a single Rhino Mesh. This is a typical pre-step before using the intersect_mesh_rays functions. Args: geometry: An array of Rhino Breps or Rhino Meshes. """ if len(geometry) == 1 and isinstance(geometry[0], rg.Mesh): return geometry[0] joined_mesh = rg.Mesh() for geo in geometry: if isinstance(geo, rg.Mesh): joined_mesh.Append(geo) elif isinstance(geo, rg.Brep): meshes = rg.Mesh.CreateFromBrep(geo, rg.MeshingParameters.Default) for mesh in meshes: joined_mesh.Append(mesh) else: # it's likely an extrusion object try: geo = geo.ToBrep() # extrusion objects must be cast to Brep in Rhino 8 meshes = rg.Mesh.CreateFromBrep(geo, rg.MeshingParameters.Default) for mesh in meshes: joined_mesh.Append(mesh) except: raise TypeError('Geometry must be either a Brep or a Mesh. ' 'Not {}.'.format(type(geo))) return joined_mesh
[docs] def join_geometry_to_gridded_mesh(geometry, grid_size, offset_distance=0): """Create a single gridded Ladybug Mesh3D from an array of Rhino geometry. Args: breps: An array of Rhino Breps and/or Rhino meshes that will be converted into a single, joined gridded Ladybug Mesh3D. grid_size: A number for the grid size dimension with which to make the mesh. offset_distance: A number for the distance at which to offset the points from the underlying geometry. The default is 0. Returns: A tuple with three elements - joined_mesh -- A Rhino Mesh from the input geometry. - points -- A list of Rhino Point3ds for the mesh face centers. - normals -- A list of Rhino Point3ds for the mesh face normals. """ # set up the meshing parameters meshing_param = rg.MeshingParameters.Default meshing_param.MaximumEdgeLength = grid_size meshing_param.MinimumEdgeLength = grid_size meshing_param.GridAspectRatio = 1 # loop through the geometry and mesh it joined_mesh = rg.Mesh() for geo in geometry: if isinstance(geo, rg.Mesh): joined_mesh.Append(geo) else: if not isinstance(geo, rg.Brep): # it's likely an extrusion object geo = geo.ToBrep() # extrusion objects must be cast to Brep in Rhino 8 mesh_grids = rg.Mesh.CreateFromBrep(geo, meshing_param) for m_grid in mesh_grids: joined_mesh.Append(m_grid) # compute the points at each face center and offset them if necessary joined_mesh.FaceNormals.ComputeFaceNormals() joined_mesh.FaceNormals.UnitizeFaceNormals() normals = [joined_mesh.FaceNormals[i] for i in range(joined_mesh.FaceNormals.Count)] points = [] if offset_distance == 0: for i, n in enumerate(normals): points.append(joined_mesh.Faces.GetFaceCenter(i)) else: od = offset_distance for i, n in enumerate(normals): pt = joined_mesh.Faces.GetFaceCenter(i) pt = rg.Point3d(pt.X + (n.X * od), pt.Y + (n.Y * od), pt.Z + (n.Z * od)) points.append(pt) return joined_mesh, points, normals
[docs] def join_geometry_to_brep(geometry): """Convert an array of Rhino Breps and/or Meshes into a single Rhino Brep. This is a typical pre-step before using the ray tracing functions. Args: geometry: An array of Rhino Breps or Rhino Meshes. """ joined_mesh = join_geometry_to_mesh(geometry) return rg.Brep.CreateFromMesh(joined_mesh, False)
[docs] def bounding_box(geometry, high_accuracy=False): """Get a Rhino bounding box around an input Rhino Mesh or Brep. This is a typical pre-step before using intersection functions. Args: geometry: A Rhino Brep or Mesh. high_accuracy: If True, a physically accurate bounding box will be computed. If not, a bounding box estimate will be computed. For some geometry types, there is no difference between the estimate and the accurate bounding box. Estimated bounding boxes can be computed much (much) faster than accurate (or "tight") bounding boxes. Estimated bounding boxes are always similar to or larger than accurate bounding boxes. """ return geometry.GetBoundingBox(high_accuracy)
[docs] def bounding_box_extents(geometry, high_accuracy=False): """Get min and max points around an input Rhino Mesh or Brep Args: geometry: A Rhino Brep or Mesh. high_accuracy: If True, a physically accurate bounding box will be computed. If not, a bounding box estimate will be computed. For some geometry types, there is no difference between the estimate and the accurate bounding box. Estimated bounding boxes can be computed much (much) faster than accurate (or "tight") bounding boxes. Estimated bounding boxes are always similar to or larger than accurate bounding boxes. """ b_box = bounding_box(geometry, high_accuracy) return b_box.Max, b_box.Min
[docs] def intersect_mesh_rays( mesh, points, vectors, normals=None, cpu_count=None, parallel=True): """Intersect a group of rays (represented by points and vectors) with a mesh. All combinations of rays that are possible between the input points and vectors will be intersected. This method exists since most CAD plugins have much more efficient mesh/ray intersection functions than ladybug_geometry. However, the ladybug_geometry Face3D.intersect_line_ray() method provides a workable (albeit very inefficient) alternative to this if it is needed. Args: mesh: A Rhino mesh that can block the rays. points: An array of Rhino points that will be used to generate rays. vectors: An array of Rhino vectors that will be used to generate rays. normals: An optional array of Rhino vectors that align with the input points and denote the direction each point is facing. These will be used to eliminate any cases where the vector and the normal differ by more than 90 degrees. If None, points are assumed to have no direction. cpu_count: An integer for the number of CPUs to be used in the intersection calculation. The ladybug_rhino.grasshopper.recommended_processor_count function can be used to get a recommendation. If set to None, all available processors will be used. (Default: None). parallel: Optional boolean to override the cpu_count and use a single CPU instead of multiple processors. Returns: A tuple with two elements - intersection_matrix -- A 2D matrix of 0's and 1's indicating the results of the intersection. Each sub-list of the matrix represents one of the points and has a length equal to the vectors. 0 indicates a blocked ray and 1 indicates a ray that was not blocked. - angle_matrix -- A 2D matrix of angles in radians. Each sub-list of the matrix represents one of the normals and has a length equal to the supplied vectors. Will be None if no normals are provided. """ intersection_matrix = [0] * len(points) # matrix to be filled with results angle_matrix = [0] * len(normals) if normals is not None else None cutoff_angle = math.pi / 2 # constant used in all normal checks if not parallel: cpu_count = 1 def intersect_point(i): """Intersect all of the vectors of a given point without any normal check.""" pt = points[i] int_list = [] for vec in vectors: ray = rg.Ray3d(pt, vec) if rg.Intersect.Intersection.MeshRay(mesh, ray) >= 0: is_clear = 0 else: is_clear = 1 int_list.append(is_clear) intersection_matrix[i] = int_list def intersect_point_normal_check(i): """Intersect all of the vectors of a given point with a normal check.""" pt, normal_vec = points[i], normals[i] int_list = [] angle_list = [] for vec in vectors: vec_angle = rg.Vector3d.VectorAngle(normal_vec, vec) angle_list.append(vec_angle) if vec_angle <= cutoff_angle: ray = rg.Ray3d(pt, vec) if rg.Intersect.Intersection.MeshRay(mesh, ray) >= 0: is_clear = 0 else: is_clear = 1 int_list.append(is_clear) else: # the vector is pointing behind the surface int_list.append(0) intersection_matrix[i] = specializedarray.array('B', int_list) angle_matrix[i] = specializedarray.array('d', angle_list) def intersect_each_point_group(worker_i): """Intersect groups of points so that only the cpu_count is used.""" start_i, stop_i = pt_groups[worker_i] for count in range(start_i, stop_i): intersect_point(count) def intersect_each_point_group_normal_check(worker_i): """Intersect groups of points with distance check so only cpu_count is used.""" start_i, stop_i = pt_groups[worker_i] for count in range(start_i, stop_i): intersect_point_normal_check(count) if cpu_count is not None and cpu_count > 1: # group the points in order to meet the cpu_count pt_count = len(points) worker_count = min((cpu_count, pt_count)) i_per_group = int(math.ceil(pt_count / worker_count)) pt_groups = [[x, x + i_per_group] for x in range(0, pt_count, i_per_group)] pt_groups[-1][-1] = pt_count # ensure the last group ends with point count if normals is not None: if cpu_count is None: # use all available CPUs tasks.Parallel.ForEach(range(len(points)), intersect_point_normal_check) elif cpu_count <= 1: # run everything on a single processor for i in range(len(points)): intersect_point_normal_check(i) else: # run the groups in a manner that meets the CPU count tasks.Parallel.ForEach( range(len(pt_groups)), intersect_each_point_group_normal_check) else: if cpu_count is None: # use all available CPUs tasks.Parallel.ForEach(range(len(points)), intersect_point) elif cpu_count <= 1: # run everything on a single processor for i in range(len(points)): intersect_point(i) else: # run the groups in a manner that meets the CPU count tasks.Parallel.ForEach(range(len(pt_groups)), intersect_each_point_group) return intersection_matrix, angle_matrix
[docs] def intersect_rays_with_mesh_faces( mesh, rays, context=None, normals=None, cpu_count=None): """Intersect a matrix of rays with a mesh to get the intersected mesh faces. This method is useful when trying to color each face of the mesh with values that can be linked to each one of the rays. For example, this method is used in all shade benefit calculations. Args: mesh: A Rhino mesh that will be intersected with the rays. rays: A matrix (list of lists) where each sublist contains Rhino Rays to be intersected with the mesh. context: An optional Rhino mesh that will be used to evaluate if the rays are blocked before performing the calculation with the input mesh. Rays that intersect this context will be discounted from the calculation. normals: An optional array of Rhino vectors that align with the input rays and denote the direction each ray group is facing. These will be used to eliminate any cases where the vector and the normal differ by more than 90 degrees. If None, points are assumed to have no direction. cpu_count: An integer for the number of CPUs to be used in the intersection calculation. The ladybug_rhino.grasshopper.recommended_processor_count function can be used to get a recommendation. If set to None, all available processors will be used. (Default: None). Returns: A 2D matrix of integers indicating the results of the intersection. Each sub-list of the matrix represents one of the mesh faces and the integers within it refer to the indices of the rays in the rays list that intersected that face. """ #create a list to populate intersected indices for each face face_int = [] for _ in range(mesh.Faces.Count): face_int.append([]) # place holder for result # process the input normals if supplied if normals is None: ang_mtx = [[True] * len(r) for r in rays] else: cutoff_angle = math.pi / 2 # constant used in all normal checks ang_mtx = [] for ray_list, normal_vec in zip(rays, normals): pt_ang = [] for ray in ray_list: vec_angle = rg.Vector3d.VectorAngle(normal_vec, ray.Direction) vec_seen = True if vec_angle <= cutoff_angle else False pt_ang.append(vec_seen) ang_mtx.append(pt_ang) def intersect_rays(i): for j, ray in enumerate(rays[i]): if ang_mtx[i][j]: face_ids = clr.StrongBox[Array[int]]() ray_p = rg.Intersect.Intersection.MeshRay(mesh, ray, face_ids) if ray_p >= 0: for indx in list(face_ids.Value): face_int[indx].append(j) def intersect_rays_context(i): for j, ray in enumerate(rays[i]): if ang_mtx[i][j] and rg.Intersect.Intersection.MeshRay(context, ray) < 0: face_ids = clr.StrongBox[Array[int]]() ray_p = rg.Intersect.Intersection.MeshRay(mesh, ray, face_ids) if ray_p >= 0: for indx in list(face_ids.Value): face_int[indx].append(j) def intersect_each_ray_group(worker_i): """Intersect groups of rays so that only the cpu_count is used.""" start_i, stop_i = ray_groups[worker_i] for count in range(start_i, stop_i): intersect_rays(count) def intersect_each_ray_group_context(worker_i): """Intersect groups of points with distance check so only cpu_count is used.""" start_i, stop_i = ray_groups[worker_i] for count in range(start_i, stop_i): intersect_rays_context(count) if cpu_count is not None and cpu_count > 1: # group the rays in order to meet the cpu_count ray_count = len(rays) worker_count = min((cpu_count, ray_count)) i_per_group = int(math.ceil(ray_count / worker_count)) ray_groups = [[x, x + i_per_group] for x in range(0, ray_count, i_per_group)] ray_groups[-1][-1] = ray_count # ensure the last group ends with ray count if context is not None: if cpu_count is None: tasks.Parallel.ForEach(range(len(rays)), intersect_rays_context) elif cpu_count <= 1: # run everything on a single processor for i in range(len(rays)): intersect_rays_context(i) else: # run the groups in a manner that meets the CPU count tasks.Parallel.ForEach( range(len(ray_groups)), intersect_each_ray_group_context) else: if cpu_count is None: tasks.Parallel.ForEach(range(len(rays)), intersect_rays) elif cpu_count <= 1: # run everything on a single processor for i in range(len(rays)): intersect_rays(i) else: # run the groups in a manner that meets the CPU count tasks.Parallel.ForEach( range(len(ray_groups)), intersect_each_ray_group) return face_int
[docs] def intersect_mesh_rays_distance(mesh, point, vectors, max_dist=None): """Intersect a group of rays with a mesh to get the distance until intersection. Args: mesh: A Rhino mesh that can block the rays. points: A Rhino point that will be used to generate rays. vectors: An array of Rhino vectors that will be used to generate rays. max_dist: An optional number to set the maximum distance beyond which context blocking the view is no longer considered relevant. If None, geometries at all distances will be evaluated for whether they block the view and the results may contain negative numbers indicating that the view from that ray is never blocked Returns: A list of values for the distance at which intersection occurs. """ distances = [] if max_dist is None: for vec in vectors: ray = rg.Ray3d(point, vec) dist = rg.Intersect.Intersection.MeshRay(mesh, ray) distances.append(dist) else: for vec in vectors: ray = rg.Ray3d(point, vec) dist = rg.Intersect.Intersection.MeshRay(mesh, ray) dist = max_dist if dist < 0 or dist > max_dist else dist distances.append(dist) return distances
[docs] def generate_intersection_rays(points, vectors): """Generate a series of rays to be used for intersection calculations. All combinations of rays between the input points and vectors will be generated. Args: points: A list of Rhino point objects for the starting point of each ray. vectors: A list of Rhino vector objects for the direction of each ray, which will be projected from each point. """ int_rays = [] for pt in points: pt_rays = [] for vec in vectors: pt_rays.append(rg.Ray3d(pt, vec)) int_rays.append(pt_rays) return int_rays
[docs] def intersect_mesh_lines( mesh, start_points, end_points, max_dist=None, cpu_count=None, parallel=True): """Intersect a group of lines (represented by start + end points) with a mesh. All combinations of lines that are possible between the input start_points and end_points will be intersected. This method exists since most CAD plugins have much more efficient mesh/line intersection functions than ladybug_geometry. However, the ladybug_geometry Face3D.intersect_line_ray() method provides a workable (albeit very inefficient) alternative to this if it is needed. Args: mesh: A Rhino mesh that can block the lines. start_points: An array of Rhino points that will be used to generate lines. end_points: An array of Rhino points that will be used to generate lines. max_dist: An optional number to set the maximum distance beyond which the end_points are no longer considered visible by the start_points. If None, points with an unobstructed view to one another will be considered visible no matter how far they are from one another. cpu_count: An integer for the number of CPUs to be used in the intersection calculation. The ladybug_rhino.grasshopper.recommended_processor_count function can be used to get a recommendation. If set to None, all available processors will be used. (Default: None). parallel: Optional boolean to override the cpu_count and use a single CPU instead of multiple processors. Returns: A 2D matrix of 0's and 1's indicating the results of the intersection. Each sub-list of the matrix represents one of the points and has a length equal to the end_points. 0 indicates a blocked ray and 1 indicates a ray that was not blocked. """ int_matrix = [0] * len(start_points) # matrix to be filled with results if not parallel: cpu_count = 1 def intersect_line(i): """Intersect a line defined by a start and an end with the mesh.""" pt = start_points[i] int_list = [] for ept in end_points: lin = rg.Line(pt, ept) int_obj = rg.Intersect.Intersection.MeshLine(mesh, lin) is_clear = 1 if None in int_obj or len(int_obj) == 0 else 0 int_list.append(is_clear) int_matrix[i] = int_list def intersect_line_dist_check(i): """Intersect a line with the mesh with a distance check.""" pt = start_points[i] int_list = [] for ept in end_points: lin = rg.Line(pt, ept) if lin.Length > max_dist: int_list.append(0) else: int_obj = rg.Intersect.Intersection.MeshLine(mesh, lin) is_clear = 1 if None in int_obj or len(int_obj) == 0 else 0 int_list.append(is_clear) int_matrix[i] = int_list def intersect_each_line_group(worker_i): """Intersect groups of lines so that only the cpu_count is used.""" start_i, stop_i = l_groups[worker_i] for count in range(start_i, stop_i): intersect_line(count) def intersect_each_line_group_dist_check(worker_i): """Intersect groups of lines with distance check so only cpu_count is used.""" start_i, stop_i = l_groups[worker_i] for count in range(start_i, stop_i): intersect_line_dist_check(count) if cpu_count is not None and cpu_count > 1: # group the lines in order to meet the cpu_count l_count = len(start_points) worker_count = min((cpu_count, l_count)) i_per_group = int(math.ceil(l_count / worker_count)) l_groups = [[x, x + i_per_group] for x in range(0, l_count, i_per_group)] l_groups[-1][-1] = l_count # ensure the last group ends with line count if max_dist is not None: if cpu_count is None: # use all available CPUs tasks.Parallel.ForEach(range(len(start_points)), intersect_line_dist_check) elif cpu_count <= 1: # run everything on a single processor for i in range(len(start_points)): intersect_line_dist_check(i) else: # run the groups in a manner that meets the CPU count tasks.Parallel.ForEach( range(len(l_groups)), intersect_each_line_group_dist_check) else: if cpu_count is None: # use all available CPUs tasks.Parallel.ForEach(range(len(start_points)), intersect_line) elif cpu_count <= 1: # run everything on a single processor for i in range(len(start_points)): intersect_line(i) else: # run the groups in a manner that meets the CPU count tasks.Parallel.ForEach( range(len(l_groups)), intersect_each_line_group) return int_matrix
[docs] def intersect_view_factor( meshes, points, vectors, vector_weights, context=None, normals=None, cpu_count=None): """Intersect a list of points with meshes to determine the view factor to each mesh. Args: meshes: A list of Rhino meshes that will be intersected to determine the view factor from each point. points: An array of Rhino points that will be used to generate rays. vectors: An array of Rhino vectors that will be used to generate rays. vector_weights: A list of numbers with the same length as the vectors corresponding to the solid angle weight of each vector. The sum of this list should be equal to one. These are needed to ensure that the resulting view factors are accurate. context: An optional Rhino mesh that will be used to evaluate if the rays are blocked before performing the calculation with the input meshes. Rays that intersect this context will be discounted from the result. normals: An optional array of Rhino vectors that align with the input points and denote the direction each point is facing. These will be used to eliminate any cases where the vector and the normal differ by more than 90 degrees and will also be used to compute view factors within the plane defined by this normal vector. If None, points are assumed to have no direction and view factors will be computed spherically around the points. cpu_count: An integer for the number of CPUs to be used in the intersection calculation. The ladybug_rhino.grasshopper.recommended_processor_count function can be used to get a recommendation. If set to None, all available processors will be used. (Default: None). Returns: A tuple with two values. - view_factors -- A 2D matrix of fractional values indicating the view factor from each point to each mesh. Each sub-list of the matrix denotes one of the input points. - mesh_indices -- A 2D matrix of integers indicating the index of each mesh struck by each view ray. Each sub-list of the matrix represents one of the points and the value in each sub-list is the integer of the mesh that was struck by a given ray shot from the point. """ # set up the matrices to be filled view_factors = [[] for _ in points] mesh_indices = [[] for _ in points] vec_count = len(vectors) cutoff_angle = math.pi / 2 # constant used in all normal checks # combine the context with the meshes if it is specified context_index = None if context is not None: meshes = list(meshes) + [context] context_index = len(meshes) - 1 def intersect_point(i): """Intersect all of the vectors of a given point without any normal check.""" # create the rays to be projected from each point rel_pt = points[i] point_rays = [] for vec in vectors: point_rays.append(rg.Ray3d(rel_pt, vec)) # perform the intersection of the rays with the mesh pt_int_mtx = [] for ray in point_rays: srf_list = [] for srf in meshes: intersect = rg.Intersect.Intersection.MeshRay(srf, ray) if intersect < 0: intersect = 'N' srf_list.append(intersect) pt_int_mtx.append(srf_list) # find the intersection that was the closest for each ray srf_hits = [[] for _ in meshes] for ray_count, int_list in enumerate(pt_int_mtx): if not all(x == 'N' for x in int_list): min_index, _ = min(enumerate(int_list), key=operator.itemgetter(1)) if min_index == context_index: mesh_indices[i].append(-1) else: mesh_indices[i].append(min_index) if normals is None or normals[i] is None: srf_hits[min_index].append(vector_weights[ray_count]) else: # get the angle between the surface and the vector vec_angle = rg.Vector3d.VectorAngle( vectors[ray_count], normals[i]) if vec_angle > cutoff_angle: srf_hits[min_index].append(0) else: srf_hits[min_index].append( vector_weights[ray_count] * 4 * abs(math.cos(vec_angle))) else: mesh_indices[i].append(-1) # sum up the lists and divide by the total rays to get the view factor for hit_list in srf_hits: view_factors[i].append(sum(hit_list) / vec_count) def intersect_each_point_group(worker_i): """Intersect groups of points so that only the cpu_count is used.""" start_i, stop_i = pt_groups[worker_i] for count in range(start_i, stop_i): intersect_point(count) if cpu_count is not None and cpu_count > 1: # group the points in order to meet the cpu_count pt_count = len(points) worker_count = min((cpu_count, pt_count)) i_per_group = int(math.ceil(pt_count / worker_count)) pt_groups = [[x, x + i_per_group] for x in range(0, pt_count, i_per_group)] pt_groups[-1][-1] = pt_count # ensure the last group ends with point count if cpu_count is None: # use all available CPUs tasks.Parallel.ForEach(range(len(points)), intersect_point) elif cpu_count <= 1: # run everything on a single processor for i in range(len(points)): intersect_point(i) else: # run the groups in a manner that meets the CPU count tasks.Parallel.ForEach(range(len(pt_groups)), intersect_each_point_group) return view_factors, mesh_indices
[docs] def trace_ray(ray, breps, bounce_count=1): """Get a list of Rhino points for the path a ray takes bouncing through breps. Args: ray: A Rhino Ray whose path will be traced through the geometry. breps: An array of Rhino breps through with the ray will be traced. bounce_count: An positive integer for the number of ray bounces to trace the sun rays forward. (Default: 1). """ return rg.Intersect.Intersection.RayShoot(ray, breps, bounce_count)
[docs] def normal_at_point(brep, point): """Get a Rhino vector for the normal at a specific point that lies on a brep. Args: breps: A Rhino brep on which the normal direction will be evaluated. point: A Rhino point on the input brep where the normal will be evaluated. """ return brep.ClosestPoint(point, tolerance)[5]
[docs] def intersect_solids_parallel(solids, bound_boxes, cpu_count=None): """Intersect the co-planar faces of an array of solids using parallel processing. Args: original_solids: An array of closed Rhino breps (polysurfaces) that do not have perfectly matching surfaces between adjacent Faces. bound_boxes: An array of Rhino bounding boxes that parallels the input solids and will be used to check whether two Breps have any potential for intersection before the actual intersection is performed. cpu_count: An integer for the number of CPUs to be used in the intersection calculation. The ladybug_rhino.grasshopper.recommended_processor_count function can be used to get a recommendation. If None, all available processors will be used. (Default: None). parallel: Optional boolean to override the cpu_count and use a single CPU instead of multiple processors. Returns: int_solids -- The input array of solids, which have all been intersected with one another. """ int_solids = solids[:] # copy the input list to avoid editing it def intersect_each_solid(i): """Intersect a solid with all of the other solids of the list.""" bb_1 = bound_boxes[i] # intersect the solids that come after this one for j, bb_2 in enumerate(bound_boxes[i + 1:]): if not overlapping_bounding_boxes(bb_1, bb_2): continue # no overlap in bounding box; intersection impossible split_brep1, int_exists = \ intersect_solid(int_solids[i], int_solids[i + j + 1]) if int_exists: int_solids[i] = split_brep1 # intersect the solids that come before this one for j, bb_2 in enumerate(bound_boxes[:i]): if not overlapping_bounding_boxes(bb_1, bb_2): continue # no overlap in bounding box; intersection impossible split_brep2, int_exists = intersect_solid(int_solids[i], int_solids[j]) if int_exists: int_solids[i] = split_brep2 def intersect_each_solid_group(worker_i): """Intersect groups of solids so that only the cpu_count is used.""" start_i, stop_i = s_groups[worker_i] for count in range(start_i, stop_i): intersect_each_solid(count) if cpu_count is None or cpu_count <= 1: # use all available CPUs tasks.Parallel.ForEach(range(len(solids)), intersect_each_solid) else: # group the solids in order to meet the cpu_count solid_count = len(int_solids) worker_count = min((cpu_count, solid_count)) i_per_group = int(math.ceil(solid_count / worker_count)) s_groups = [[x, x + i_per_group] for x in range(0, solid_count, i_per_group)] s_groups[-1][-1] = solid_count # ensure the last group ends with solid count tasks.Parallel.ForEach(range(len(s_groups)), intersect_each_solid_group) return int_solids
[docs] def intersect_solids(solids, bound_boxes): """Intersect the co-planar faces of an array of solids. Args: original_solids: An array of closed Rhino breps (polysurfaces) that do not have perfectly matching surfaces between adjacent Faces. bound_boxes: An array of Rhino bounding boxes that parallels the input solids and will be used to check whether two Breps have any potential for intersection before the actual intersection is performed. Returns: int_solids -- The input array of solids, which have all been intersected with one another. """ int_solids = solids[:] # copy the input list to avoid editing it for i, bb_1 in enumerate(bound_boxes): for j, bb_2 in enumerate(bound_boxes[i + 1:]): if not overlapping_bounding_boxes(bb_1, bb_2): continue # no overlap in bounding box; intersection impossible # split the first solid with the second one split_brep1, int_exists = intersect_solid( int_solids[i], int_solids[i + j + 1]) int_solids[i] = split_brep1 # split the second solid with the first one if an intersection was found if int_exists: split_brep2, int_exists = intersect_solid( int_solids[i + j + 1], int_solids[i]) int_solids[i + j + 1] = split_brep2 return int_solids
[docs] def intersect_solid(solid, other_solid): """Intersect the co-planar faces of one solid Brep using another. Args: solid: The solid Brep which will be split with intersections. other_solid: The other Brep, which will be used to split. Returns: A tuple with two elements - solid -- The input solid, which has been split. - intersection_exists -- Boolean to note whether an intersection was found between the solid and the other_solid. If False, there's no need to split the other_solid with the input solid. """ # variables to track the splitting process intersection_exists = False # boolean to note whether an intersection exists temp_brep = solid.Split(other_solid, tolerance) if len(temp_brep) != 0: solid = rg.Brep.JoinBreps(temp_brep, tolerance)[0] solid.Faces.ShrinkFaces() intersection_exists = True return solid, intersection_exists
[docs] def overlapping_bounding_boxes(bound_box1, bound_box2): """Check if two Rhino bounding boxes overlap within the tolerance. This is particularly useful as a check before performing computationally intense intersection processes between two bounding boxes. Checking the overlap of the bounding boxes is extremely quick given this method's use of the Separating Axis Theorem. This method is built into the intersect_solids functions in order to improve its calculation time. Args: bound_box1: The first bound_box to check. bound_box2: The second bound_box to check. """ # Bounding box check using the Separating Axis Theorem bb1_width = bound_box1.Max.X - bound_box1.Min.X bb2_width = bound_box2.Max.X - bound_box2.Min.X dist_btwn_x = abs(bound_box1.Center.X - bound_box2.Center.X) x_gap_btwn_box = dist_btwn_x - (0.5 * bb1_width) - (0.5 * bb2_width) bb1_depth = bound_box1.Max.Y - bound_box1.Min.Y bb2_depth = bound_box2.Max.Y - bound_box2.Min.Y dist_btwn_y = abs(bound_box1.Center.Y - bound_box2.Center.Y) y_gap_btwn_box = dist_btwn_y - (0.5 * bb1_depth) - (0.5 * bb2_depth) bb1_height = bound_box1.Max.Z - bound_box1.Min.Z bb2_height = bound_box2.Max.Z - bound_box2.Min.Z dist_btwn_z = abs(bound_box1.Center.Z - bound_box2.Center.Z) z_gap_btwn_box = dist_btwn_z - (0.5 * bb1_height) - (0.5 * bb2_height) if x_gap_btwn_box > tolerance or y_gap_btwn_box > tolerance or \ z_gap_btwn_box > tolerance: return False # no overlap return True # overlap exists
[docs] def split_solid_to_floors(building_solid, floor_heights): """Extract a series of planar floor surfaces from solid building massing. Args: building_solid: A closed brep representing a building massing. floor_heights: An array of float values for the floor heights, which will be used to generate planes that subdivide the building solid. Returns: floor_breps -- A list of planar, horizontal breps representing the floors of the building. """ # get the floor brep at each of the floor heights. floor_breps = [] for hgt in floor_heights: story_breps = [] floor_base_pt = rg.Point3d(0, 0, hgt) section_plane = rg.Plane(floor_base_pt, rg.Vector3d.ZAxis) floor_crvs = rg.Brep.CreateContourCurves(building_solid, section_plane) try: # Assume a single contour curve has been found floor_brep = rg.Brep.CreatePlanarBreps(floor_crvs, tolerance) except TypeError: # An array of contour curves has been found floor_brep = rg.Brep.CreatePlanarBreps(floor_crvs) if floor_brep is not None: story_breps.extend(floor_brep) floor_breps.append(story_breps) return floor_breps
[docs] def geo_min_max_height(geometry): """Get the min and max Z values of any input object. This is useful as a pre-step before the split_solid_to_floors method. """ # intersection functions changed in Rhino 7.15 such that we now need 2* tolerance add_val = tolerance * 2 if (7, 15) <= rhino_version < (7, 17) else 0 bound_box = geometry.GetBoundingBox(rg.Plane.WorldXY) return bound_box.Min.Z + add_val, bound_box.Max.Z