def test_stl_importer_2_boxes(): r"""Import an iges file containing 2 distinct boxes and test topology Notes ----- This shows the current limitations of the IgesImporter as 2 boxes cannot be distinguished from one another """ # binary STL importer = StlImporter( path_from_file(__file__, "./models_in/2_boxes_binary.stl")) topo = Topo(importer.shape, return_iter=False) # assert len(topo.solids) == 2 assert len(topo.shells) == 2 assert topo.shells[0].Closed() is True assert topo.shells[1].Closed() is True assert topo.number_of_faces == 108 * 2 assert topo.number_of_edges == 162 * 2 # ascii STL importer = StlImporter( path_from_file(__file__, "./models_in/2_boxes_ascii.stl")) topo = Topo(importer.shape, return_iter=False) # assert len(topo.solids()) == 2 assert len(topo.shells) == 2 assert topo.shells[0].Closed() is True assert topo.shells[1].Closed() is True assert topo.number_of_faces == 108 * 2 assert topo.number_of_edges == 162 * 2
def test_inclusion(): r"""Test inclusion of point in bounding box and of point is solid""" assert point_in_boundingbox(sphere_, gp_Pnt(sphere_radius - 1., sphere_radius - 1., sphere_radius - 1.)) is True assert point_in_solid(sphere_, gp_Pnt(sphere_radius - 1., 0, 0)) is True assert point_in_solid(sphere_, gp_Pnt(sphere_radius - 1., sphere_radius - 1., sphere_radius - 1.)) is False assert point_in_solid(sphere_, gp_Pnt(sphere_radius, 0, 0)) is None with pytest.raises(WrongTopologicalType): point_in_solid(edge, gp_Pnt(sphere_radius, 0, 0)) with pytest.raises(WrongTopologicalType): point_in_solid(Topo(sphere_, return_iter=False).faces[0], gp_Pnt(sphere_radius, 0, 0)) sphere_shell = Topo(sphere_, return_iter=False).shells[0] assert point_in_boundingbox(sphere_shell, gp_Pnt(sphere_radius - 1., sphere_radius - 1., sphere_radius - 1.)) is True assert point_in_solid(sphere_shell, gp_Pnt(sphere_radius - 1., 0, 0)) is True assert point_in_solid(sphere_shell, gp_Pnt(sphere_radius - 1., sphere_radius - 1., sphere_radius - 1.)) is False assert point_in_solid(sphere_shell, gp_Pnt(sphere_radius, 0, 0)) is None
def test_step_exporter_overwrite(box_shape): r"""Happy path with a subclass of TopoDS_Shape""" filename = path_from_file(__file__, "./models_out/box.stp") exporter = StepExporter(filename) solid = shape_to_topology(box_shape) assert isinstance(solid, TopoDS_Solid) exporter.add_shape(solid) exporter.write_file() initial_timestamp = os.path.getmtime(filename) assert os.path.isfile(filename) # read the written box.stp importer = StepImporter(filename) topo_compound = Topo(importer.compound, return_iter=False) assert topo_compound.number_of_faces == 6 assert len(topo_compound.faces) == 6 assert topo_compound.number_of_edges == 12 # add a sphere and write again with same exporter sphere = BRepPrimAPI_MakeSphere(10) exporter.add_shape(sphere.Shape()) exporter.write_file() # this creates a file with a box and a sphere intermediate_timestamp = os.path.getmtime(filename) assert intermediate_timestamp > initial_timestamp # check that the file contains the box and the sphere importer = StepImporter(filename) # 6 from box + 1 from sphere assert len(Topo(importer.compound, return_iter=False).faces) == 7 assert len(Topo(importer.compound, return_iter=False).solids) == 2 # create a new exporter and overwrite with a box only filename = path_from_file(__file__, "./models_out/box.stp") exporter = StepExporter(filename) solid = shape_to_topology(box_shape) exporter.add_shape(solid) exporter.write_file() assert os.path.isfile(filename) last_timestamp = os.path.getmtime(filename) assert last_timestamp > intermediate_timestamp # check the file only contains a box importer = StepImporter(filename) # 6 from box assert len(Topo(importer.compound, return_iter=False).faces) == 6 assert len(Topo(importer.compound, return_iter=False).solids) == 1
def through_face_vertices(cls, _face): r"""Fit a plane through face vertices Parameters ---------- _face : occutils.core.face.Face Returns ------- Geom_Plane """ uvs_from_vertices = [_face.project_vertex(Vertex.to_pnt(i)) for i in Topo(_face).vertices] normals = [OCC.gp.gp_Vec(_face.DiffGeom.normal(*uv[0])) for uv in uvs_from_vertices] points = [i[1] for i in uvs_from_vertices] NORMALS = OCC.TColgp.TColgp_SequenceOfVec() # [NORMALS.Append(i) for i in normals] for i in normals: NORMALS.Append(i) POINTS = to_tcol_(points, OCC.TColgp.TColgp_HArray1OfPnt) pl = OCC.GeomPlate.GeomPlate_BuildAveragePlane(NORMALS, POINTS).Plane().GetObject() vec = OCC.gp.gp_Vec(pl.Location(), _face.GlobalProperties.centre()) pt = (pl.Location().as_vec() + vec).as_pnt() pl.SetLocation(pt) return cls(pl)
def test_iges_importer_happy_topology(): r"""import iges file containing a box and test topology""" importer = IgesImporter(path_from_file(__file__, "./models_in/box.igs")) topo = Topo(importer.compound, return_iter=False) assert topo.number_of_faces == 6 assert topo.number_of_edges == 24 # 12 edges * 2 possible orientations ?
def test_global_properties_box(): r"""Properties of a the box""" # wrap the box in GlobalProperties box_properties = GlobalProperties(box_) # check the volume assert box_properties.volume == box_dim_x * box_dim_y * box_dim_z # check the length is not defined for the box with pytest.raises(WrongTopologicalType): box_properties.length # check the area is not defined for the box .... with pytest.raises(WrongTopologicalType): box_properties.area # .... but the area of the shell of the box is defined and exact box_shell = Topo(box_, return_iter=False).shells[0] shell_properties = GlobalProperties(box_shell) theoretical_area = 2 * box_dim_x * box_dim_y + \ 2 * box_dim_y * box_dim_z + \ 2 * box_dim_x * box_dim_z assert theoretical_area - tol <= shell_properties.area <= theoretical_area + tol # but the length is not defined for a shell.... with pytest.raises(WrongTopologicalType): shell_properties.length # ... nor is the volume with pytest.raises(WrongTopologicalType): shell_properties.volume
def analyse(self): r"""Bad edges of the shell""" bad_edges = list() ss = OCC.ShapeAnalysis.ShapeAnalysis_Shell() ss.LoadShells(self._wrapped_instance) if ss.HasFreeEdges(): bad_edges = [e for e in Topo(ss.BadEdges()).edges] return bad_edges
def __init__(self, wire_a, wire_b): self.wireA = wire_a self.wireB = wire_b self.wire_explorer_a = WireExplorer(self.wireA) self.wire_explorer_b = WireExplorer(self.wireB) self.topo_a = Topo(self.wireA) self.topo_b = Topo(self.wireB) self.brep_tool = OCC.BRep.BRep_Tool() self.vertices_a = [v for v in self.wire_explorer_a.ordered_vertices] self.vertices_b = [v for v in self.wire_explorer_b.ordered_vertices] self.edges_a = [v for v in WireExplorer(wire_a).ordered_edges] self.edges_b = [v for v in WireExplorer(wire_b).ordered_edges] self.pnts_b = [self.brep_tool.Pnt(v) for v in self.vertices_b] self.number_of_vertices = len(self.vertices_a) self.index = 0
def real_bb_position(axis, side, start_position, shape, increment=0.01): r"""Workaround for OCC bounding box imprecision. The principle is to move a plane (perpendicular to axis) closer and closer until it intersects the shape. The goal is to get a 'sure to intersect' coordinates for another program. Parameters ---------- axis : str in ["X", "Y", "Z"] side : str, in ["MIN", "MAX"] The side from which we try to intersect the shape start_position : float shape : OCC Shape increment : float, optional The distance by which the intersection plane is moved to try to intersect the shape Default is 0.01 Returns ------- float The value of the position for the specified axis and side ("MIN" or "MAX") """ if axis not in ["X", "Y", "Z"]: raise ValueError("axis must be 'X', 'Y' or 'Z'") if side not in ["MIN", "MAX"]: raise ValueError("side must be 'MIN' or 'MAX'") plane_builders = {"X": build_plane_at_x, "Y": build_plane_at_y, "Z": build_plane_at_z} plane_builder = plane_builders[axis] position = start_position intersect = False while intersect is False: plane = plane_builder(position, shape) common_shape = common(shape, plane) list_vertex = Topo(common_shape, return_iter=False).vertices if len(list_vertex) >= 1: intersect = True else: if side == "MIN": position += increment elif side == "MAX": position -= increment # Bug correction : make sure the computed bounding box is wider # than the shape by a value between 0 and increment if side == "MIN": return position - increment elif side == "MAX": return position + increment
def topo(self): r"""Topo Returns ------- occutils.topology.Topo """ return Topo(self._wrapped_instance)
def shells(self): r"""Shells making the solid Returns ------- list[Shell] """ return (Shell(sh) for sh in Topo(self._wrapped_instance))
def test_step_importer_2_boxes(): r"""Import an step file containing 2 distinct boxes and test topology""" importer = StepImporter( path_from_file(__file__, "./models_in/2_boxes_203.stp")) assert len(importer.shapes) == 1 assert importer.shapes[0].ShapeType() == TopAbs_COMPOUND topo = Topo(importer.shapes[0]) assert topo.number_of_compounds == 1 assert topo.number_of_comp_solids == 0 assert topo.number_of_solids == 2 assert topo.number_of_shells == 2
def test_stl_importer_happy_topology(): r"""import iges file containing a box and test topology""" # binary STL importer = StlImporter(path_from_file(__file__, "./models_in/box_binary.stl")) topo = Topo(importer.shape, return_iter=False) # assert len(topo.solids()) == 1 assert len(topo.shells) == 1 assert topo.shells[0].Closed() is True # direct method on TopoDS_Shell assert len(topo.faces) == 108 assert len(topo.edges) == 162 # ascii STL importer = StlImporter(path_from_file(__file__, "./models_in/box_ascii.stl")) topo = Topo(importer.shape, return_iter=False) # assert len(topo.solids) == 1 assert len(topo.shells) == 1 assert topo.shells[0].Closed() is True assert len(topo.faces) == 108 assert len(topo.edges) == 162
def test_stl_exporter_overwrite(box_shape): r"""Happy path with a subclass of TopoDS_Shape""" filename = path_from_file(__file__, "./models_out/box.stl") exporter = StlExporter(filename) solid = shape_to_topology(box_shape) assert isinstance(solid, TopoDS_Solid) exporter.set_shape(solid) exporter.write_file() assert os.path.isfile(filename) # read the written box.stl importer = StlImporter(filename) topo = Topo(importer.shape) assert topo.number_of_shells == 1 # set a sphere and write again with same exporter sphere = BRepPrimAPI_MakeSphere(10) exporter.set_shape(sphere.Shape()) # this creates a file with a sphere only, this is STL specific exporter.write_file() # check that the file contains the sphere only importer = StlImporter(filename) topo = Topo(importer.shape) assert topo.number_of_shells == 1 # create a new exporter and overwrite with a box only filename = path_from_file(__file__, "./models_out/box.stl") exporter = StlExporter(filename) solid = shape_to_topology(box_shape) exporter.set_shape(solid) exporter.write_file() assert os.path.isfile(filename) # check the file only contains a box importer = StlImporter(filename) topo = Topo(importer.shape) assert topo.number_of_shells == 1
def test_iges_exporter_overwrite(box_shape): r"""Happy path with a subclass of TopoDS_Shape""" filename = path_from_file(__file__, "./models_out/box.igs") exporter = IgesExporter(filename) solid = shape_to_topology(box_shape) assert isinstance(solid, TopoDS_Solid) exporter.add_shape(solid) exporter.write_file() assert os.path.isfile(filename) # read the written box.igs importer = IgesImporter(filename) topo_compound = Topo(importer.compound) assert topo_compound.number_of_faces == 6 assert topo_compound.number_of_edges == 24 # add a sphere and write again with same exporter sphere = BRepPrimAPI_MakeSphere(10) exporter.add_shape(sphere.Shape()) exporter.write_file() # this creates a file with a box and a sphere # check that the file contains the box and the sphere importer = IgesImporter(filename) topo_compound = Topo(importer.compound) assert topo_compound.number_of_faces == 7 # 6 from box + 1 from sphere # create a new exporter and overwrite with a box only filename = path_from_file(__file__, "./models_out/box.igs") exporter = IgesExporter(filename) solid = shape_to_topology(box_shape) exporter.add_shape(solid) exporter.write_file() assert os.path.isfile(filename) # check the file only contains a box importer = IgesImporter(filename) topo_compound = Topo(importer.compound) assert topo_compound.number_of_faces == 6 # 6 from box
def test_iges_importer_2_boxes(): r"""Import an iges file containing 2 distinct boxes and test topology Notes ----- This shows the current limitations of the IgesImporter as 2 boxes cannot be distinguished from one another """ importer = IgesImporter(path_from_file(__file__, "./models_in/2_boxes.igs")) topo = Topo(importer.compound, return_iter=False) assert topo.number_of_faces == 6 * 2 assert topo.number_of_edges == 24 * 2
def test_step_importer_happy_topology(): r"""import step file containing a box and test topology""" importer = StepImporter(path_from_file(__file__, "./models_in/box_203.stp")) assert len(importer.shapes) == 1 assert isinstance(importer.shapes[0], TopoDS_Shape) assert importer.shapes[0].ShapeType() == TopAbs_SOLID topo = Topo(importer.shapes[0]) assert topo.number_of_compounds == 0 assert topo.number_of_comp_solids == 0 assert topo.number_of_solids == 1 assert topo.number_of_shells == 1
def edges(display, shape, width=4, show_numbers=True, numbers_height=20, color_sequence=None): r"""Display each edge of shape in a different color Parameters ---------- display : Reference to display shape : OCC.TopoDS.TopoDS_Shape width : int Edge width for display show_numbers : bool Show the numbering of faces numbers_height : int Height of displayed numbers if show_numbers is True color_sequence : aocutils.display.color.*_sequence """ if color_sequence is None: color_sequence = prism_color_sequence the_edges = Topo(shape, return_iter=False).edges logger.info("%i edges(s) to display" % len(the_edges)) ais_context = display.GetContext().GetObject() for i, edge in enumerate(the_edges): ais_edge = OCC.AIS.AIS_Shape(edge) ais_edge.SetWidth(width) ais_edge.SetColor(color_sequence[i % len(color_sequence)]) if show_numbers: display.DisplayMessage(point=Edge(edge).midpoint, text_to_write=str(i), height=numbers_height, message_color=(0, 0, 0)) ais_context.Display(ais_edge.GetHandle())
def shells(display, shape, transparency=0., color_sequence=None): r"""Display each shell of shape in a different color Parameters ---------- display : Reference to display shape : OCC.TopoDS.TopoDS_Shape transparency : float color_sequence : aocutils.display.color.*_sequence """ if color_sequence is None: color_sequence = prism_color_sequence the_shells = Topo(shape, return_iter=False).shells logger.info("%i shell(s) to display" % len(the_shells)) ais_context = display.GetContext().GetObject() for i, shell in enumerate(the_shells): ais_face = OCC.AIS.AIS_Shape(shell) ais_face.SetColor(color_sequence[i % len(color_sequence)]) ais_face.SetTransparency(transparency) ais_context.Display(ais_face.GetHandle())
def test_check_shape(): r"""check_shape() tests""" # Null shapes should raise a ValueError with pytest.raises(ValueError): check_shape(TopoDS_Shape()) with pytest.raises(ValueError): check_shape(TopoDS_Shell()) builderapi_makeedge = BRepBuilderAPI_MakeEdge(gp_Pnt(), gp_Pnt(10, 10, 10)) shape = builderapi_makeedge.Shape() # a ValueError should be raised is check_shape() is not give # a TopoDS_Shape or subclass with pytest.raises(ValueError): check_shape(gp_Pnt()) with pytest.raises(ValueError): check_shape(builderapi_makeedge) # a TopoDS_Shape should pass the check without raising any exception check_shape(shape) # a subclass of shape should not raise any exception check_shape(Topo(shape, return_iter=False).edges[0])
def faces(display, shape, transparency=0., show_numbers=True, numbers_height=20, color_sequence=None): r"""Display each face of shape in a different color Parameters ---------- display : Reference to display shape : OCC.TopoDS.TopoDS_Shape transparency : float show_numbers : bool Show the numbering of faces numbers_height : int Height of displayed numbers if show_numbers is True color_sequence : aocutils.display.color.*_sequence """ if color_sequence is None: color_sequence = prism_color_sequence the_faces = Topo(shape, return_iter=False).faces logger.info("%i face(s) to display" % len(the_faces)) ais_context = display.GetContext().GetObject() for i, face in enumerate(the_faces): ais_face = OCC.AIS.AIS_Shape(face) ais_face.SetColor(color_sequence[i % len(color_sequence)]) ais_face.SetTransparency(transparency) if show_numbers: display.DisplayMessage(point=Face(face).midpoint, text_to_write=str(i), height=numbers_height, message_color=(0, 0, 0)) ais_context.Display(ais_face.GetHandle())
def wires(self): r"""Wires of the shell""" return Topo(self._wrapped_instance, return_iter=True).wires
def edges(self): r"""Edges of the shell""" return Topo(self._wrapped_instance, return_iter=True).edges
def wires(display, shape, width=4, show_numbers=True, numbers_height=50, repeat=2, delay=1., color_sequence=None): r"""Display each edge of shape in a different color Parameters ---------- display : Reference to display shape : OCC.TopoDS.TopoDS_Shape width : int Wire width for display show_numbers : bool Show the numbering of faces numbers_height : int Height of displayed numbers if show_numbers is True repeat : int Number of times to repeat the display sequence delay : float Number of seconds a wire can be visualized color_sequence : aocutils.display.color.*_sequence Notes ----- Wires may overlap or a wire may cover another because one or more wires use the same edge. This causes the display of wires to be confusing. This is the reason why this function displays each wire in turn. """ if color_sequence is None: color_sequence = prism_color_sequence the_wires = Topo(shape, return_iter=False).wires logger.info("%i wire(s) to display" % len(the_wires)) ais_context = display.GetContext().GetObject() # make sure the zoom is about right display.DisplayShape(shape) display.FitAll() # for n in range(repeat): for _ in range(repeat): for i, wire in enumerate(the_wires): display.EraseAll() ais_edge = OCC.AIS.AIS_Shape(wire) ais_edge.SetWidth(width) ais_edge.SetColor(color_sequence[i % len(color_sequence)]) ais_context.Display(ais_edge.GetHandle()) if show_numbers: first_edge_of_wire = Topo(wire, return_iter=False).edges[0] wrapped_first_edge = Edge(first_edge_of_wire) display.DisplayMessage(point=wrapped_first_edge.midpoint, text_to_write=str(i), height=numbers_height, message_color=(0, 0, 0), update=True) time.sleep(delay) # wait before displaying the next wire
def read_file(self): r"""Read file""" logger.info("Reading STEP file") h_doc = Handle_TDocStd_Document() # Create the application app = _XCAFApp.XCAFApp_Application_GetApplication().GetObject() app.NewDocument(TCollection_ExtendedString("MDTV-CAF"), h_doc) # Get root assembly doc = h_doc.GetObject() h_shape_tool = XCAFDoc_DocumentTool().ShapeTool(doc.Main()) color_tool = XCAFDoc_DocumentTool().ColorTool(doc.Main()) layer_tool = XCAFDoc_DocumentTool().LayerTool(doc.Main()) _ = XCAFDoc_DocumentTool().MaterialTool(doc.Main()) step_reader = STEPCAFControl_Reader() step_reader.SetColorMode(True) step_reader.SetLayerMode(True) step_reader.SetNameMode(True) step_reader.SetMatMode(True) status = step_reader.ReadFile(str(self.filename)) if status == IFSelect_RetDone: logger.info("Transfer doc to STEPCAFControl_Reader") step_reader.Transfer(doc.GetHandle()) labels = TDF_LabelSequence() _ = TDF_LabelSequence() # TopoDS_Shape a_shape; _ = h_shape_tool.GetObject() h_shape_tool.GetObject().GetFreeShapes(labels) logger.info('Number of shapes at root :%i' % labels.Length()) # for i in range(labels.Length()): # a_shape = h_shape_tool.GetObject().GetShape(labels.Value(i+1)) # logger.debug("%i - type : %s" % (i, a_shape.ShapeType())) # sub_shapes_labels = TDF_LabelSequence() # print("Is Assembly?", shape_tool.IsAssembly(labels.Value(i + 1))) # # sub_shapes = shape_tool.getsubshapes(labels.Value(i+1), # sub_shapes_labels) # # sub_shapes = shape_tool.FindSubShape(labels.Value(i + 1), # a_shape, labels.Value(i + 1)) # print('Number of subshapes in the assembly : %i' % # sub_shapes_labels.Length()) # # color_tool.GetObject().GetColors(color_labels) # logger.info('Number of colors : %i' % color_labels.Length()) for i in range(labels.Length()): # print i label = labels.Value(i + 1) logger.debug("Label : %s" % label) a_shape = h_shape_tool.GetObject().GetShape(labels.Value(i + 1)) # string_seq = TColStd_HSequenceOfExtendedString() # string_seq is an TColStd_HSequenceOfExtendedString string_seq = layer_tool.GetObject().GetLayers(a_shape) color = Quantity_Color() _ = color_tool.GetObject().GetColor(a_shape, XCAFDoc_ColorSurf, color) logger.info("The shape type is : %i" % a_shape.ShapeType()) if a_shape.ShapeType() == TopAbs_COMPOUND: logger.info("The shape type is TopAbs_COMPOUND") topo = Topo(a_shape) logger.info("Nb of compounds : %i" % topo.number_of_compounds) logger.info("Nb of solids : %i" % topo.number_of_solids) logger.info("Nb of shells : %i" % topo.number_of_shells) for solid in topo.solids: logger.info("Adding solid to the shapes list") self._shapes.append(solid) elif a_shape.ShapeType() == TopAbs_SOLID: logger.info("The shape type is TopAbs_SOLID") self._shapes.append(a_shape) self._colors.append(color) self._layers.append(string_seq) return True
class LoopWirePairs(object): r"""For looping through consecutive wires assures that the returned edge pairs are ordered Parameters ---------- wire_a : OCC.TopoDS.TopoDS_Wire wire_b : OCC.TopoDS.TopoDS_Wire """ def __init__(self, wire_a, wire_b): self.wireA = wire_a self.wireB = wire_b self.wire_explorer_a = WireExplorer(self.wireA) self.wire_explorer_b = WireExplorer(self.wireB) self.topo_a = Topo(self.wireA) self.topo_b = Topo(self.wireB) self.brep_tool = OCC.BRep.BRep_Tool() self.vertices_a = [v for v in self.wire_explorer_a.ordered_vertices] self.vertices_b = [v for v in self.wire_explorer_b.ordered_vertices] self.edges_a = [v for v in WireExplorer(wire_a).ordered_edges] self.edges_b = [v for v in WireExplorer(wire_b).ordered_edges] self.pnts_b = [self.brep_tool.Pnt(v) for v in self.vertices_b] self.number_of_vertices = len(self.vertices_a) self.index = 0 def closest_point(self, vertex_from_wire_a): r"""Closest vertex in the wire b to a vertex from wire a Parameters ---------- vertex_from_wire_a Returns ------- OCC.TopoDS.TopoDS_Vertex """ pt = self.brep_tool.Pnt(vertex_from_wire_a) distances = [pt.Distance(i) for i in self.pnts_b] indx_max_dist = distances.index(min(distances)) return self.vertices_b[indx_max_dist] def __next__(self): r"""next() method to make LoopWirePairs an iterable Returns ------- """ if self.index == self.number_of_vertices: raise StopIteration vert = self.vertices_a[self.index] closest = self.closest_point(vert) edges_a = self.topo_a.edges_from_vertex(vert) edges_b = self.topo_b.edges_from_vertex(closest) edge_a1, edge_a2 = Edge(edges_a.next()), Edge(edges_a.next()) edge_b1, edge_b2 = Edge(edges_b.next()), Edge(edges_b.next()) mp_a = edge_a1.mid_point()[1] self.index += 1 if mp_a.Distance(edge_b1.mid_point()[1]) < mp_a.Distance( edge_b2.mid_point()[1]): return iter([edge_a1, edge_a2]), iter([edge_b1, edge_b2]) else: return iter([edge_a1, edge_a2]), iter([edge_b2, edge_b1]) def __iter__(self): return self # 2 / 3 compatibility next = __next__
def faces(self): r"""Faces of the shell""" return Topo(self._wrapped_instance, return_iter=True).faces