def associate_inner_boundaries(fc_boundaries, doc): """Associate parent boundary and inner boundaries""" to_delete = [] for fc_boundary in fc_boundaries: if not fc_boundary.IsHosted or fc_boundary.ParentBoundary: continue host_boundaries = [] for host_element in fc_boundary.RelatedBuildingElement.HostElements: host_boundaries.extend(host_element.ProvidesBoundaries) candidates = set(fc_boundaries).intersection(host_boundaries) try: host = get_host(fc_boundary, candidates) except InvalidBoundary as err: logger.exception(err) to_delete.append(fc_boundary) continue fc_boundary.ParentBoundary = host if host: utils.append(host, "InnerBoundaries", fc_boundary) # Remove invalid boundary and corresponding inner wire updated_boundaries = fc_boundaries[:] for boundary in to_delete: remove_invalid_inner_wire(boundary, updated_boundaries) updated_boundaries.remove(boundary) doc.removeObject(boundary.Name)
def get_medial_axis(boundary1, boundary2, ei1, ei2) -> Optional[Part.Line]: line1 = utils.line_from_edge(utils.get_outer_wire(boundary1).Edges[ei1]) try: line2 = utils.line_from_edge( utils.get_outer_wire(boundary2).Edges[ei2]) except IndexError: logger.warning( f"""Cannot find closest edge index <{ei2}> in boundary <{boundary2.Label}> to rejoin boundary <{boundary1.Label}>""") return None # Case 2a : edges are not parallel if abs(line1.Direction.dot(line2.Direction)) < 1 - TOLERANCE: b1_plane = utils.get_plane(boundary1) line_intersect = line1.intersect2d(line2, b1_plane) if line_intersect: point1 = b1_plane.value(*line_intersect[0]) if line1.Direction.dot(line2.Direction) > 0: point2 = point1 + line1.Direction + line2.Direction else: point2 = point1 + line1.Direction - line2.Direction # Case 2b : edges are parallel else: point1 = (line1.Location + line2.Location) * 0.5 point2 = point1 + line1.Direction try: return Part.Line(point1, point2) except Part.OCCError: logger.exception( f"Failure in boundary id <{boundary1.SourceBoundary.Id}> {point1} and {point2} are equal" ) return None
def merge_boundaries(boundary1, boundary2) -> bool: """Try to merge 2 boundaries. Retrun True if successfully merged""" wire1 = utils.get_outer_wire(boundary1) wire2 = utils.get_outer_wire(boundary2) new_wire, extra_inner_wires = merged_wires(wire1, wire2) if not new_wire: return False # Update shape if boundary1.IsHosted: utils.remove_inner_wire(boundary1.ParentBoundary, wire1) utils.remove_inner_wire(boundary2.ParentBoundary, wire2) utils.append_inner_wire(boundary1.ParentBoundary, new_wire) else: for inner_boundary in boundary2.InnerBoundaries: utils.append(boundary1, "InnerBoundaries", inner_boundary) inner_boundary.ParentBoundary = boundary1 inner_wires = utils.get_inner_wires(boundary1)[:] inner_wires.extend(utils.get_inner_wires(boundary2)) inner_wires.extend(extra_inner_wires) try: utils.generate_boundary_compound(boundary1, new_wire, inner_wires) except RuntimeError as error: logger.exception(error) return False RelSpaceBoundary.recompute_areas(boundary1) return True
def ensure_hosted_element_are(space, doc): for boundary in space.SecondLevel.Group: try: ifc_type = boundary.RelatedBuildingElement.IfcType except AttributeError: continue if not is_typically_hosted(ifc_type): continue if boundary.IsHosted and boundary.ParentBoundary: continue def valid_hosts(boundary): """Guess valid hosts""" for boundary2 in space.SecondLevel.Group: if boundary is boundary2 or is_typically_hosted( boundary2.IfcType): continue if not boundary2.Area.Value - boundary.Area.Value >= 0: continue if not utils.are_parallel_boundaries(boundary, boundary2): continue if utils.are_too_far(boundary, boundary2): continue yield boundary2 def find_host(boundary): fallback_solution = None for boundary2 in valid_hosts(boundary): fallback_solution = boundary2 for inner_wire in utils.get_inner_wires(boundary2): if (not abs( Part.Face(inner_wire).Area - boundary.Area.Value) < TOLERANCE): continue return boundary2 if not fallback_solution: raise HostNotFound( f"No host found for RelSpaceBoundary Id<{boundary.Id}>") logger.warning( f"Using fallback solution to resolve host of RelSpaceBoundary Id<{boundary.Id}>" ) return fallback_solution try: host = find_host(boundary) except HostNotFound as err: host = create_fake_host(boundary, space, doc) logger.exception(err) boundary.IsHosted = True boundary.ParentBoundary = host utils.append(host, "InnerBoundaries", boundary)
def associate_host_element(ifc_file, elements_group): # Associate Host / Hosted elements ifc_elements = (e for e in ifc_file.by_type("IfcElement") if e.ProvidesBoundaries) for ifc_entity in ifc_elements: if ifc_entity.FillsVoids: try: host = utils.get_element_by_guid( utils.get_host_guid(ifc_entity), elements_group) except LookupError as err: logger.exception(err) continue hosted = utils.get_element_by_guid(ifc_entity.GlobalId, elements_group) utils.append(host, "HostedElements", hosted) utils.append(hosted, "HostElements", host)
def rejoin_boundaries(space, sia_type): """ Rejoin boundaries after their translation to get a correct close shell surfaces. 1 Fill gaps between boundaries (2b) 2 Fill gaps gerenate by translation to make a boundary on the inside or outside boundary of building elements https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/schema/ifcproductextension/lexical/ifcrelspaceboundary2ndlevel.htm # pylint: disable=line-too-long """ base_boundaries = space.SecondLevel.Group for base_boundary in base_boundaries: boundary1 = getattr(base_boundary, sia_type) if not boundary1: continue lines = [] fallback_lines = [ utils.line_from_edge(edge) for edge in utils.get_outer_wire(boundary1).Edges ] # bound_box used to make sure line solution is in a reallistic scope (distance <= 5 m) bound_box = boundary1.Shape.BoundBox bound_box.enlarge(5000) if (base_boundary.IsHosted or base_boundary.PhysicalOrVirtualBoundary == "VIRTUAL" or not base_boundary.RelatedBuildingElement): continue b1_plane = utils.get_plane(boundary1) for b2_id, (ei1, ei2), fallback_line in zip( base_boundary.ClosestBoundaries, enumerate(base_boundary.ClosestEdges), fallback_lines, ): base_boundary2 = utils.get_in_list_by_id(base_boundaries, b2_id) boundary2 = getattr(base_boundary2, sia_type, None) if not boundary2: logger.warning( f"Cannot find corresponding boundary with id <{b2_id}>") lines.append(fallback_line) continue # Case 1 : boundaries are not parallel line = get_intersecting_line(boundary1, boundary2) if line: if not is_valid_join(line, fallback_line): line = fallback_line if not bound_box.intersect(line.Location, line.Direction): line = fallback_line lines.append(line) continue # Case 2 : boundaries are parallel line = get_medial_axis(boundary1, boundary2, ei1, ei2) if line and is_valid_join(line, fallback_line): lines.append(line) continue lines.append(fallback_line) # Generate new shape try: outer_wire = utils.polygon_from_lines(lines, b1_plane) except (Part.OCCError, utils.ShapeCreationError): logger.exception( f"Invalid geometry while rejoining boundary Id <{base_boundary.Id}>" ) continue try: Part.Face(outer_wire) except Part.OCCError: logger.exception( f"Unable to rejoin boundary Id <{base_boundary.Id}>") continue inner_wires = utils.get_inner_wires(boundary1) try: utils.generate_boundary_compound(boundary1, outer_wire, inner_wires) except RuntimeError as err: logger.exception(err) continue boundary1.Area = area = boundary1.Shape.Area for inner_boundary in base_boundary.InnerBoundaries: area = area + inner_boundary.Shape.Area boundary1.AreaWithHosted = area