def merge_coplanar_boundaries(boundaries: list, doc=FreeCAD.ActiveDocument): """Try to merge coplanar boundaries""" if len(boundaries) == 1: return boundary1 = max(boundaries, key=lambda x: x.Area) # Ensure all boundaries are coplanar plane = utils.get_plane(boundary1) for boundary in boundaries: utils.project_boundary_onto_plane(boundary, plane) boundaries.remove(boundary1) remove_from_doc = list() # Attempt to merge boundaries while True and boundaries: for boundary2 in boundaries: if merge_boundaries(boundary1, boundary2): merge_corresponding_boundaries(boundary1, boundary2) boundaries.remove(boundary2) remove_from_doc.append(boundary2) break else: logger.warning( f"""Unable to merge boundaries RelSpaceBoundary Id <{boundary1.Id}> with boundaries <{", ".join(str(b.Id) for b in boundaries)}>""" ) break # Clean FreeCAD document if join operation was a success for fc_object in remove_from_doc: doc.removeObject(fc_object.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 handle_curtain_walls(space, doc) -> None: """Add an hosted window with full area in curtain wall boundaries as they are not handled by BEM softwares""" for boundary in space.SecondLevel.Group: if getattr(boundary.RelatedBuildingElement, "IfcType", "") != "IfcCurtainWall": continue # Prevent Revit issue which produce curtain wall with an hole inside but no inner boundary if not boundary.InnerBoundaries: if len(boundary.Shape.SubShapes) > 2: outer_wire = boundary.Shape.SubShapes[1] utils.generate_boundary_compound(boundary, outer_wire, ()) boundary.LesoType = "Wall" fake_window = doc.copyObject(boundary) fake_window.IsHosted = True fake_window.LesoType = "Window" fake_window.ParentBoundary = boundary fake_window.GlobalId = ifcopenshell.guid.new() fake_window.Id = IfcId.new(doc) RelSpaceBoundary.set_label(fake_window) space.SecondLevel.addObject(fake_window) # Host cannot be an empty face so inner wire is scaled down a little inner_wire = utils.get_outer_wire(boundary).scale(0.999) inner_wire = utils.project_wire_to_plane(inner_wire, utils.get_plane(boundary)) utils.append_inner_wire(boundary, inner_wire) utils.append(boundary, "InnerBoundaries", fake_window) if FreeCAD.GuiUp: fake_window.ViewObject.ShapeColor = (0.0, 0.7, 1.0)
def find_closest_by_intersection(boundary1, boundary2): intersect_line = utils.get_plane(boundary1).intersectSS( utils.get_plane(boundary2))[0] boundaries_distance = boundary1.Shape.distToShape(boundary2.Shape)[0] edges1 = utils.get_outer_wire(boundary1).Edges edges2 = utils.get_outer_wire(boundary2).Edges for (ei1, edge1), (ei2, edge2) in itertools.product(enumerate(edges1), enumerate(edges2)): distance1 = edge_distance_to_line(edge1, intersect_line) + boundaries_distance distance2 = edge_distance_to_line(edge2, intersect_line) + boundaries_distance min_distance = boundary1.Proxy.closest[ei1].distance if distance1 < min_distance: boundary1.Proxy.closest[ei1] = Closest(boundary2, -1, distance1) min_distance = boundary2.Proxy.closest[ei2].distance if distance2 < min_distance: boundary2.Proxy.closest[ei2] = Closest(boundary1, -1, distance2)
def ensure_hosted_are_coplanar(space): for boundary in space.SecondLevel.Group: inner_wires = utils.get_inner_wires(boundary) missing_inner_wires = False if len(inner_wires) < len(boundary.InnerBoundaries): missing_inner_wires = True outer_wire = utils.get_outer_wire(boundary) for inner_boundary in boundary.InnerBoundaries: if utils.is_coplanar(inner_boundary, boundary) and not missing_inner_wires: continue utils.project_boundary_onto_plane(inner_boundary, utils.get_plane(boundary)) inner_wire = utils.get_outer_wire(inner_boundary) inner_wires.append(inner_wire) try: utils.generate_boundary_compound(boundary, outer_wire, inner_wires) except RuntimeError: continue
def create_fake_host(boundary, space, doc): fake_host = doc.copyObject(boundary) fake_host.IsHosted = False fake_host.LesoType = "Wall" fake_host.GlobalId = ifcopenshell.guid.new() fake_host.Id = IfcId.new(doc) RelSpaceBoundary.set_label(fake_host) space.SecondLevel.addObject(fake_host) inner_wire = utils.get_outer_wire(boundary) outer_wire = inner_wire.scaled(1.001, inner_wire.CenterOfMass) plane = utils.get_plane(boundary) outer_wire = utils.project_wire_to_plane(outer_wire, plane) inner_wire = utils.project_wire_to_plane(inner_wire, plane) utils.generate_boundary_compound(fake_host, outer_wire, [inner_wire]) boundary.ParentBoundary = fake_host fake_building_element = doc.copyObject(boundary.RelatedBuildingElement) fake_building_element.Id = IfcId.new(doc) fake_host.RelatedBuildingElement = fake_building_element utils.append(fake_host, "InnerBoundaries", boundary) if FreeCAD.GuiUp: fake_host.ViewObject.ShapeColor = (0.7, 0.3, 0.0) return fake_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
def get_intersecting_line(boundary1, boundary2) -> Optional[Part.Line]: plane_intersect = utils.get_plane(boundary1).intersectSS( utils.get_plane(boundary2)) return plane_intersect[0] if plane_intersect else None