예제 #1
0
def associate_corresponding_boundary(boundary, doc):
    """Associate corresponding boundaries according to IFC definition.

    Reference to the other space boundary of the pair of two space boundaries on either side of a
    space separating thermal boundary element.
    https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML/link/ifcrelspaceboundary2ndlevel.htm
    """
    if (boundary.InternalOrExternalBoundary != "INTERNAL"
            or boundary.CorrespondingBoundary):
        return

    candidates = cleaned_corresponding_candidates(boundary)
    corresponding_boundary = get_best_corresponding_candidate(
        boundary, candidates)
    if corresponding_boundary:
        boundary.CorrespondingBoundary = corresponding_boundary
        corresponding_boundary.CorrespondingBoundary = boundary
    elif boundary.PhysicalOrVirtualBoundary == "VIRTUAL" and seems_too_smal(
            boundary):
        logger.warning(f"""
    Boundary {boundary.Label} from space {boundary.RelatingSpace.Id} has been removed.
    It is VIRTUAL, INTERNAL, thin and has no corresponding boundary. It looks like a parasite."""
                       )
        doc.removeObject(boundary.Name)
    else:
        # Considering test above. Assume that it has been missclassified but log the issue.
        boundary.InternalOrExternalBoundary = "EXTERNAL"
        logger.warning(f"""
    No corresponding boundary found for {boundary.Label} from space {boundary.RelatingSpace.Id}.
    Assigning to EXTERNAL assuming it was missclassified as INTERNAL""")
예제 #2
0
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
예제 #3
0
def define_leso_type(boundary):
    try:
        ifc_type = boundary.RelatedBuildingElement.IfcType
    except AttributeError:
        if boundary.PhysicalOrVirtualBoundary != "VIRTUAL":
            logger.warning(
                f"Unable to define LesoType for boundary <{boundary.Id}>")
        return "Unknown"
    if ifc_type.startswith("IfcWindow"):
        return "Window"
    elif ifc_type.startswith("IfcDoor"):
        return "Door"
    elif ifc_type.startswith("IfcWall"):
        return "Wall"
    elif ifc_type.startswith("IfcSlab") or ifc_type == "IfcRoof":
        # Pointing up => Ceiling. Pointing down => Flooring
        if boundary.Normal.z > 0:
            return "Ceiling"
        return "Flooring"
    elif ifc_type.startswith("IfcOpeningElement"):
        return "Opening"
    else:
        logger.warning(
            f"Unable to define LesoType for Boundary Id <{boundary.Id}>")
        return "Unknown"
예제 #4
0
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)
예제 #5
0
def group_by_shared_element(boundaries) -> Dict[str, List["boundary"]]:
    elements_dict = dict()
    for rel_boundary in boundaries:
        try:
            key = f"{rel_boundary.RelatedBuildingElement.Id}_{rel_boundary.InternalOrExternalBoundary}"
        except AttributeError:
            if rel_boundary.PhysicalOrVirtualBoundary == "VIRTUAL":
                logger.info("IfcElement %s is VIRTUAL. Modeling error ?")
                key = "VIRTUAL"
            else:
                logger.warning("IfcElement %s has no RelatedBuildingElement",
                               rel_boundary.Id)
        corresponding_boundary = rel_boundary.CorrespondingBoundary
        if corresponding_boundary:
            key += str(corresponding_boundary.Id)
        elements_dict.setdefault(key, []).append(rel_boundary)
    return elements_dict
예제 #6
0
        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
예제 #7
0
def merge_over_splitted_boundaries(space, doc=FreeCAD.ActiveDocument):
    """Try to merge oversplitted boundaries to reduce the number of boundaries and make sure that
    windows are not splitted as it is often with some authoring softwares like Revit.
    Why ? Less boundaries is more manageable, closer to what user expect and require
    less computational power"""
    boundaries = space.SecondLevel.Group
    # Considered as the minimal size for an oversplit to occur (1 ceiling, 3 wall, 1 flooring)
    if len(boundaries) <= 5:
        return
    elements_dict = group_by_shared_element(boundaries)

    # Merge hosted elements first
    for key, boundary_list in elements_dict.items():
        if boundary_list[0].IsHosted and len(boundary_list) != 1:
            coplanar_groups = group_coplanar_boundaries(boundary_list)
            for group in coplanar_groups:
                merge_coplanar_boundaries(group, doc)

    for key, boundary_list in elements_dict.items():
        # None coplanar boundaries should not be connected.
        # eg. round wall splitted with multiple orientations.

        # Case1: No oversplitted boundaries
        try:
            if boundary_list[0].IsHosted or len(boundary_list) == 1:
                continue
        except ReferenceError:
            continue

        coplanar_groups = group_coplanar_boundaries(boundary_list)

        for group in coplanar_groups:
            # Case 1 : only 1 boundary related to the same element. Cannot group boundaries.
            if len(group) == 1:
                continue
            # Case 2 : more than 1 boundary related to the same element might be grouped.
            try:
                merge_coplanar_boundaries(group, doc)
            except Part.OCCError:
                logger.warning(
                    f"Cannot join boundaries in space <{space.Id}> with key <{key}>"
                )
예제 #8
0
    def generate_space(self, ifc_space, parent):
        """Generate Space and RelSpaceBoundaries as defined in ifc_file. No post process."""
        fc_space = Space.create_from_ifc(ifc_space, self)
        parent.addObject(fc_space)

        boundaries = fc_space.newObject("App::DocumentObjectGroup",
                                        "Boundaries")
        fc_space.Boundaries = boundaries
        second_levels = boundaries.newObject("App::DocumentObjectGroup",
                                             "SecondLevel")
        fc_space.SecondLevel = second_levels

        # All boundaries have their placement relative to space placement
        space_placement = self.get_placement(ifc_space)
        for ifc_boundary in (b for b in ifc_space.BoundedBy
                             if is_second_level(b)):
            if not ifc_boundary.ConnectionGeometry:
                logger.warning(
                    f"Boundary <{ifc_boundary.id()}> has no ConnectionGeometry and has therefore been ignored"
                )
                continue
            try:
                fc_boundary = RelSpaceBoundary.create_from_ifc(
                    ifc_entity=ifc_boundary, ifc_importer=self)
                fc_boundary.RelatingSpace = fc_space
                second_levels.addObject(fc_boundary)
                fc_boundary.Placement = space_placement
            except utils.ShapeCreationError:
                logger.warning(
                    f"Failed to create fc_shape for RelSpaceBoundary <{ifc_boundary.id()}> even with fallback methode _part_by_mesh. IfcOpenShell bug ?"
                )
            except utils.IsTooSmall:
                logger.warning(
                    f"Boundary <{ifc_boundary.id()}> shape is too small and has been ignored"
                )
예제 #9
0
    def guess_thickness(self, obj, ifc_entity):
        if obj.Material:
            thickness = getattr(obj.Material, "TotalThickness", 0)
            if thickness:
                return thickness

        if ifc_entity.is_a("IfcWall"):
            qto_lookup_name = "Qto_WallBaseQuantities"
        elif ifc_entity.is_a("IfcSlab"):
            qto_lookup_name = "Qto_SlabBaseQuantities"
        else:
            qto_lookup_name = ""

        if qto_lookup_name:
            for definition in ifc_entity.IsDefinedBy:
                if not definition.is_a("IfcRelDefinesByProperties"):
                    continue
                if definition.RelatingPropertyDefinition.Name == qto_lookup_name:
                    for quantity in definition.RelatingPropertyDefinition.Quantities:
                        if quantity.Name == "Width":
                            return quantity.LengthValue * self.fc_scale * self.ifc_scale

        if not getattr(ifc_entity, "Representation", None):
            return 0
        if ifc_entity.IsDecomposedBy:
            thicknesses = []
            for aggregate in ifc_entity.IsDecomposedBy:
                thickness = 0
                for related in aggregate.RelatedObjects:
                    thickness += self.guess_thickness(obj, related)
                thicknesses.append(thickness)
            return max(thicknesses)
        for representation in ifc_entity.Representation.Representations:
            if (representation.RepresentationIdentifier == "Box"
                    and representation.RepresentationType == "BoundingBox"):
                if self.is_wall_like(obj.IfcType):
                    return representation.Items[
                        0].YDim * self.fc_scale * self.ifc_scale
                elif self.is_slab_like(obj.IfcType):
                    return representation.Items[
                        0].ZDim * self.fc_scale * self.ifc_scale
                else:
                    return 0
        try:
            fc_shape = self.element_local_shape_by_brep(ifc_entity)
            bbox = fc_shape.BoundBox
        except RuntimeError:
            return 0
        # Returning bbox thickness for windows or doors is not insteresting
        # as it does not return frame thickness.
        if self.is_wall_like(obj.IfcType):
            return min(bbox.YLength, bbox.XLength)
        elif self.is_slab_like(obj.IfcType):
            return bbox.ZLength
        # Here we consider that thickness is distance between the 2 faces with higher area
        elif ifc_entity.is_a("IfcRoof"):
            faces = sorted(fc_shape.Faces, key=lambda x: x.Area)
            if len(faces) < 2:
                logger.warning(
                    f"""{ifc_entity.is_a()}<{ifc_entity.id()}> has an invalid geometry (empty or less than 2 faces)"""
                )
                return 0
            return faces[-1].distToShape(faces[-2])[0]
        return 0
예제 #10
0
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