diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index fa058834e4..31750774cc 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -1404,7 +1404,7 @@ class ShapeBuilder: return self.file.createIfcTriangulatedFaceSet(Coordinates=ifc_points, CoordIndex=ifc_faces) def polygonal_face_set( - self, points: SequenceOfVectors, faces: Sequence[Sequence[int]] + self, points: SequenceOfVectors, faces: Sequence[Union[Sequence[int], Sequence[Sequence[int]]]] ) -> ifcopenshell.entity_instance: """ Generate an IfcPolygonalFaceSet @@ -1413,10 +1413,33 @@ class ShapeBuilder: :param points: list of 3d coordinates :param faces: list of faces consisted of point indices (points indices starting from 0) + in case of multiple sequences per face, the subsequent ones are inner voids :return: IfcPolygonalFaceSet """ + + def is_sequence_of_ints(x): + return isinstance(x, Sequence) and not isinstance(x, (str, bytes)) and all(isinstance(el, int) for el in x) + + def is_sequence_of_sequence_of_ints(x): + return ( + isinstance(x, Sequence) and not isinstance(x, (str, bytes)) and all(is_sequence_of_ints(el) for el in x) + ) + + def incr(face): + return [i + 1 for i in face] + + if not all(is_sequence_of_ints(f) or is_sequence_of_sequence_of_ints(f) for f in faces): + raise ValueError("Expected a sequence of int or sequence of sequence of int for each face") + ifc_points = self.file.createIfcCartesianPointList3D(ifc_safe_vector_type(points)) - ifc_faces = [self.file.createIfcIndexedPolygonalFace([i + 1 for i in face]) for face in faces] + ifc_faces = [ + ( + self.file.createIfcIndexedPolygonalFace(incr(face)) + if is_sequence_of_ints(face) + else self.file.createIfcIndexedPolygonalFaceWithVoids(incr(face[0]), list(map(incr, face[1:]))) + ) + for face in faces + ] return self.file.createIfcPolygonalFaceSet(Coordinates=ifc_points, Faces=ifc_faces) def extrude_face_set( diff --git a/src/ifcopenshell-python/test/util/test_shape_builder.py b/src/ifcopenshell-python/test/util/test_shape_builder.py index 4313c37507..4a573d454a 100644 --- a/src/ifcopenshell-python/test/util/test_shape_builder.py +++ b/src/ifcopenshell-python/test/util/test_shape_builder.py @@ -373,3 +373,55 @@ class TestCalculateTransitions(test.bootstrap.IFC4): # method C params["offset"][0] = 10.0 self.calculate_and_test(params, None) + + +class TestFaceset(test.bootstrap.IFC4): + @pytest.mark.parametrize("with_inner", [False, True]) + def test_polygonal_face_set_simple_and_with_voids(self, with_inner): + self.builder = ShapeBuilder(self.file) + + v0 = (0.0, 0.0, 0.0) + v1 = (4.0, 0.0, 0.0) + v2 = (4.0, 4.0, 0.0) + v3 = (0.0, 4.0, 0.0) + + v4 = (1.0, 1.0, 0.0) + v5 = (3.0, 1.0, 0.0) + v6 = (3.0, 3.0, 0.0) + v7 = (1.0, 3.0, 0.0) + + if with_inner: + points = [v0, v1, v2, v3, v4, v5, v6, v7] + + faces = [ + [[0, 1, 2, 3], [4, 5, 6, 7]], # outer loop with inner hole + ] + else: + points = [v0, v1, v2, v3] + + faces = [[0, 1, 2, 3]] # only outer loop + + result = self.builder.polygonal_face_set(points, faces) + + assert result.is_a("IfcPolygonalFaceSet") + assert result.Coordinates.is_a("IfcCartesianPointList3D") + assert len(result.Faces) == 1 + if with_inner: + assert result.Faces[0].is_a("IfcIndexedPolygonalFaceWithVoids") + else: + assert result.Faces[0].is_a("IfcIndexedPolygonalFace") + + shp = ifcopenshell.geom.create_shape(ifcopenshell.geom.settings(), result) + if with_inner: + assert ifcopenshell.util.shape.get_area(shp) == pytest.approx(12.0) + else: + assert ifcopenshell.util.shape.get_area(shp) == pytest.approx(16.0) + + def test_polygonal_face_set_invalid_face_types(self): + self.builder = ShapeBuilder(self.file) + with pytest.raises(ValueError, match="Expected a sequence of int or sequence of sequence of int"): + self.builder.polygonal_face_set([], ["123"]) + with pytest.raises(ValueError, match="Expected a sequence of int or sequence of sequence of int"): + self.builder.polygonal_face_set([], [[1.0, 2.0, 3.0]]) + with pytest.raises(ValueError, match="Expected a sequence of int or sequence of sequence of int"): + self.builder.polygonal_face_set([], [[[[1, 2], 3], [4, 5, 6]]])