---------------- Indexed Face Set ---------------- GeneralizedIndexedFaceSet ========================= This *Wiki* shows how to generate an indexed face set and how to modify it, using the tools provided by *pyddg*. In this example will we do this by running python in a terminal. At first we import the indexedfaceset module from pyddg: .. doctest:: >>> import ddg.datastructures.indexedfaceset.ifs as ifs Now we're ready to generate our first indexed face set. .. doctest:: >>> faceSet = ifs.GeneralizedIndexedFaceSet([(1, 2, 4), (2, 3, 4)]) Our example is a triangulated square with vertices 1 2 3 4 in cyclic order. Faces and edges will always appear as (lists respectively sets of) tuples by using :py:meth:`~ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet.face_list`. .. doctest:: >>> faceSet.get_vertex_set() {1, 2, 3, 4} >>> faceSet.face_list() [(1, 2, 4), (2, 3, 4)] >>> sorted(faceSet.edge_set()) [(1, 2), (1, 4), (2, 3), (2, 4), (3, 4)] The edges get added to the edge_list by following the boundary of the faces in the input order while the boundary of a face are all tuples of edges with cyclic orientation. .. doctest:: >>> faceSet.face_boundary((1,2,4)) ((1, 2), (2, 4), (4, 1)) Note that edges in the edge_list are sorted tuples and in the face_boundary they rather follow an orientation of the face. We can simply add a face. .. doctest:: >>> faceSet.add_face((1,3,4)) >>> faceSet.face_list() [(1, 2, 4), (2, 3, 4), (1, 3, 4)] >>> faceSet.number_of_faces() 3 .. image:: minimal_ifs.png :width: 300px :align: center Neighbors and opposite faces can be accessed. .. doctest:: >>> faceSet.opposite_face((1,3,4), (4,1)) (1, 2, 4) >>> faceSet.adjacent_faces((1,4)) [(1, 2, 4), (1, 3, 4)] >>> faceSet.adjacent_faces((4,1)) [(1, 2, 4), (1, 3, 4)] As edge tuples are always sorted there is no orientation of the faces given. The IFS Generator ----------------- All objects created in the :py:mod:`~ddg.datastructures.indexedfaceset.ifs_generator` module use methods from the :py:mod:`~ddg.math.geometric_objects` module. The module has generator functions for faces and coordinates of geometric objects. The :py:mod:`~ddg.datastructures.indexedfaceset.ifs_generator` is a module for simple construction of common geometric objects. We can create platonic solids using: .. doctest:: >>> import numpy as np >>> import ddg.datastructures.indexedfaceset.ifs_generator as gen >>> tetrahedron = gen.tetrahedron() >>> cube = gen.cube() >>> octahedron = gen.octahedron() >>> dodecahedron = gen.dodecahedron() >>> icosahedron = gen.icosahedron() .. image:: platonic_solids.png :width: 800px :align: center One can choose to not create vertex coordinates by setting the ``generate_coordinates=False`` flag. .. doctest:: >>> cube = gen.cube(generate_coordinates=False) One can also create a :py:meth:`~ddg.datastructures.indexedfaceset.ifs_generator.cylinder` .. doctest:: >>> import ddg.datastructures.indexedfaceset.ifs_generator as gen >>> cylinder = gen.cylinder(resolution=20, generate_coordinates=True, top_radius=1, bot_radius=1, ... length=1, center=(0,0,0), normal=(0,0,1)) an :py:meth:`~ddg.datastructures.indexedfaceset.ifs_generator.arrow` .. doctest:: >>> import ddg.datastructures.indexedfaceset.ifs_generator as gen >>> arrow = gen.arrow(resolution=20, generate_coordinates=True, ... heights=(0,0.7,0.7,1), radii=(0.05,0.05,0.125)) or a :py:meth:`~ddg.datastructures.indexedfaceset.ifs_generator.disc` .. doctest:: >>> import ddg.datastructures.indexedfaceset.ifs_generator as gen >>> disc = gen.disc(resolution=20, generate_coordinates=True, center=(0,0,0), normal=(0,0,1), radius=1) .. image:: cylinder_arrow_disc.png :width: 400px :align: center Attributes ========== Setting attributes to cells --------------------------- The attributes assigned to each cell are stored as instance attributes :py:attr:`~ddg.datastructures.indexedfaceset.ifs.GeneralizedIndexedFaceSet.general_vertex_attributes`, :py:attr:`~ddg.datastructures.indexedfaceset.ifs.GeneralizedIndexedFaceSet.general_edge_attributes` and :py:attr:`~ddg.datastructures.indexedfaceset.ifs.GeneralizedIndexedFaceSet.general_face_attributes`. The attributes can be assigned to cell using the method :py:meth:`~ddg.datastructures.indexedfaceset.ifs.GeneralizedIndexedFaceSet.set_attribute` which has the name of the attribute together with the cell type and the values of the attributes to be assigned as parameters: .. doctest:: >>> from ddg.datastructures.indexedfaceset.ifs import GeneralizedIndexedFaceSet as ifs >>> faceSet = ifs([(1, 2, 4), (2, 3, 4, 5), (1, 4, 7)]) >>> vertex_attr = [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1]] >>> faceSet.set_attribute("coord", "verts", vertex_attr) >>> faceSet.general_vertex_attributes {'coord': {1: [0, 0, 0], 2: [0, 0, 1], 3: [0, 1, 0], 4: [0, 1, 1], 5: [1, 0, 0], 7: [1, 0, 1]}} Different attributes will be stored as different keys to the dictionary: .. doctest:: >>> height_attribute = [10, 20, 30, 40, 50, 60] >>> faceSet.set_attribute("height", "verts", height_attribute) >>> faceSet.general_vertex_attributes {'coord': {1: [0, 0, 0], 2: [0, 0, 1], 3: [0, 1, 0], 4: [0, 1, 1], 5: [1, 0, 0], 7: [1, 0, 1]}, 'height': {1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 7: 60}} The attributes can be also assigned only to a subset of the cells by specifyiing the optional parameter in :py:meth:`~ddg.datastructures.indexedfaceset.ifs.GeneralizedIndexedFaceSet.set_attribute`: .. doctest:: >>> vertex_subset = {1, 2, 4} >>> attribute_subset = [[1, 1, 1], [1, 1, 2], [1, 1, 4]] Here two cases arise. First case would be when the attribute to be assigned to the subset does not already exist: .. doctest:: >>> faceSet.set_attribute("new_attr", "verts", attribute_subset, vertex_subset) >>> faceSet.general_vertex_attributes {'coord': {1: [0, 0, 0], 2: [0, 0, 1], 3: [0, 1, 0], 4: [0, 1, 1], 5: [1, 0, 0], 7: [1, 0, 1]}, 'height': {1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 7: 60}, 'new_attr': {1: [1, 1, 1], 2: [1, 1, 2], 4: [1, 1, 4]}} Or the case when the attributes already exists. In this case the attributes is updated at the specified cells: .. doctest:: >>> faceSet.set_attribute("coord", "verts", attribute_subset, vertex_subset) >>> faceSet.general_vertex_attributes {'coord': {1: [1, 1, 1], 2: [1, 1, 2], 3: [0, 1, 0], 4: [1, 1, 4], 5: [1, 0, 0], 7: [1, 0, 1]}, 'height': {1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 7: 60}, 'new_attr': {1: [1, 1, 1], 2: [1, 1, 2], 4: [1, 1, 4]}} This feature is similar for edges and faces. .. doctest:: >>> area_attributes = [[100], [150], [200]] >>> faceSet.set_attribute("area", "faces", area_attributes) >>> faceSet.general_face_attributes {'area': {(1, 2, 4): [100], (2, 3, 4, 5): [150], (1, 4, 7): [200]}} In order to delete an attribute from the dictionary, the method :py:meth:`~ddg.datastructures.indexedfaceset.ifs.GeneralizedIndexedFaceSet.delete_attribute` can be used: .. doctest:: >>> faceSet.delete_attribute("new_attr", "verts") >>> faceSet.general_vertex_attributes {'coord': {1: [1, 1, 1], 2: [1, 1, 2], 3: [0, 1, 0], 4: [1, 1, 4], 5: [1, 0, 0], 7: [1, 0, 1]}, 'height': {1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 7: 60}} Getting and Setting attributes ------------------------------ Getting and setting attributes for single cells can be done using methods :py:meth:`~ddg.datastructures.indexedfaceset.ifs.GeneralizedIndexedFaceSet.get_attribute` and :py:meth:`~ddg.datastructures.indexedfaceset.ifs.GeneralizedIndexedFaceSet.set_attribute`. Notice that here the cells themselves must be given and not their indices: .. doctest:: >>> faceSet.get_attribute("height", "verts", 7) 60 >>> faceSet.get_attribute("area", "faces", (1, 4, 7)) [200] In IndexFaceSet objects, indices can also be given to the methods (see below). Another important thing to notice is that the "set" method ignores any type or size missmatch between the new attribute for the cell and the attributes of other cells, therefore the user must be alert of the types and sizes: .. doctest:: >>> faceSet.set_attribute("area", "faces", {500, 200}, (1, 4, 7)) >>> faceSet.general_face_attributes {'area': {(1, 2, 4): [100], (2, 3, 4, 5): [150], (1, 4, 7): {200, 500}}} Oriented indexed face sets ========================== We can initialize a faceSet that comes with an orientation for every faces according to the input order of vertices. Opposite edges (those edges that are adjacent to the same vertices but belong to different faces) will always have to direct into different directions. Therefore these edges are called "halfedges". .. doctest:: >>> import ddg.datastructures.indexedfaceset.ifs as ifs >>> faceSet = ifs.OrientedIndexedFaceSet([(1, 2, 4), (2, 3, 4),(1, 4, 3)]) .. image:: minimal_oriented_ifs.png :width: 300px :align: center By construction every edge uniquely determines its face. We can look for all adjacent faces of an edge which will always result in a list including one face. .. doctest:: >>> faceSet.adjacent_faces((2,4)) [(1, 2, 4)] >>> faceSet.adjacent_faces((4,2)) [(2, 3, 4)] The edge_list will look the same as in the non-oriented case i.e. including sorted tuples, for every two opposite halfedges one edge tuple. We can not change the orientation of faces but we can view them as instances of the class "OrientedFace". If, for example, we ask for OrientedFaces that are adjacent to a given edge, i.e. (2,4), we will get two faces but the face (2,3,4) with reversed orientation. .. doctest:: >>> faceSet.adjacent_faces_with_orientation((4,2)) [, ] >>> [f.get_tuple() for f in faceSet.adjacent_faces_with_orientation((2,4))] [(1, 2, 4), (2, 3, 4)] By calling the tuple itself we cannot draw conclusions on their orientation. Therefore the face tuple with the correct vertex order can be accessed by OrientedFace.get_oriented_tuple() and further the input orientation will be denoted by 1 and a reversed orientation by -1 when calling OrientedFace.get_orientation(). .. doctest:: >>> [f.get_orientation() for f in faceSet.adjacent_faces_with_orientation((2,4))] [1, -1] >>> [f.get_oriented_tuple() for f in faceSet.adjacent_faces_with_orientation((2,4))] [(1, 2, 4), (4, 3, 2)] As above we can ask for all neighboring faces of a faces while their orientation (1 or -1) will be determined by the orientation of the edges in the initial face. .. doctest:: >>> [f.get_oriented_tuple() for f in faceSet.neighboring_faces_with_orientation((1,2,4))] [None, (4, 3, 2), (3, 4, 1)] opposite_face_with_orientation will work in the same way. As input it requires a face and a sorted edge tuple that has to fit to the orientation of the face. If it does so, it will return the face opposite to the given edge with its orientation determined by the input edge. .. doctest:: >>> faceSet.opposite_face_with_orientation((1,2,4),(2,4)).get_oriented_tuple() (4, 3, 2) >>> faceSet.opposite_face_with_orientation((1,2,4),(4,2)) Traceback (most recent call last): ... ValueError: Edge is not an edge of the face Converting indexed face sets ============================ This section will deal with the automatic conversion from an indexed face set to an oriented indexed face set and from either one to a surface, i.e. a :ref:`half_edge` object. For these we need the utils module. .. doctest:: >>> import ddg.datastructures.indexedfaceset.ifs as ifs >>> import ddg.datastructures.indexedfaceset.utils as utils Firstly one can (this will be done implicitly in the following conversions and raise an Value Error if False) check if the given indexed face set is a manifold, namely if every undirected edge of the indexed face set is contained in at most two faces. .. doctest:: >>> faceSet = ifs.GeneralizedIndexedFaceSet([(1, 2, 4), (2, 3, 4),(1, 3, 4)]) >>> utils.is_manifold(faceSet) True Since oriented face sets require the manifold property by construction this will always hold for them but has to be checked for manually generated (non-oriented) indexed face sets. For a non-oriented face set we can call utils.orient(IndexedFaceSet). This will, starting with the first face, do a breadth-first search to orient all further faces depending on the previous orientations. If possible it will return the oriented face set as an object of the class *OrientedIndexedFaceSet* and if not it will raise a Value Error. .. doctest:: >>> utils.orient(faceSet).face_list() [(1, 2, 4), (2, 3, 4), (4, 3, 1)] The class *OrientedIndexedFaceSet* includes same methods as in *IndexedFaceSet* except that the add_face function will also make sure that the given face does not conflicts with the manifold structure. Finally we can convert the faceSet to a half edge data structure. It orients the faceSet and thus checks the manifold property. If the indexed face set can not be converted an empty half edge data structure is returned. The function call is:: indexed_face_set_to_surface(ifs, vertex_index_attribute='ifs_index', face_index_attribute='ifs_face_index') such that the vertices and faces have index attributes based on the indices in the faceSet which can be named. .. doctest:: >>> hds = utils.indexed_face_set_to_surface(faceSet) >>> [f.ifs_face_index for f in hds.faces] [0, 1, 2] >>> [v.ifs_index for v in hds.verts] [1, 2, 3, 4] IndexedFaceSet ============== These are types of ifs that has enumerated vertices from 0 to n-1. They tend to work with numpy arrays anywhere possible. For example the edges are not stored as tuples but as 2D numpy arrays. Similarly, if the faces are all same type of polygon, the faces are also stored as 2D numpy array. The cells are stored in instance attributes :py:attr:`~ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet.verts`, :py:attr:`~ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet.edges` and :py:attr:`~ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet.faces` to be comparable with their counterpart in halfedge surfaces. .. doctest:: >>> from ddg.datastructures.indexedfaceset.ifs import IndexedFaceSet as ifs >>> faceSet = ifs([(1, 2, 3), (4, 0, 2), (1, 5, 3, 0)]) >>> faceSet2 = ifs([(1, 2, 3), (4, 0, 2), (1, 5, 3)]) >>> faceSet.verts array([0, 1, 2, 3, 4, 5]) >>> faceSet.edges array([[0, 1], [2, 4], [1, 2], [0, 4], [1, 5], [0, 3], [2, 3], [0, 2], [1, 3], [3, 5]]) >>> faceSet.faces array([(1, 2, 3), (4, 0, 2), (1, 5, 3, 0)], dtype=object) >>> faceSet2.faces # doctest: +NORMALIZE_WHITESPACE array([[1, 2, 3], [4, 0, 2], [1, 5, 3]], dtype=object) Notice for **IndexedFaceSet** data type of face arrays are *object* and therefore the verices cannot be used as indices directly. The user must change the `dtype` to any `int` to be able to use them as indices. This have been automatically resolve in **NgonalIndexedFaceSet**. Setting attributes to cells --------------------------- The same method as the general case can be used to set attributes to cell. This method however is the overriden method of the parent class: unlike the parent class, we will not store the attributes as dictionaries with cells as keys. We will however store the whole numpy array in a dictionary. Additionally, attributes are stored in :py:attr:`~ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet.vertex_attributes`, :py:attr:`~ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet.edge_attributes` and :py:attr:`~ddg.datastructures.indexedfaceset.ifs.IndexedFaceSet.face_attributes`. To clarify consider the following exmaple: .. doctest:: >>> vertex_attr = [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1]] >>> faceSet.set_attribute("coord", "verts", vertex_attr) >>> faceSet.vertex_attributes # doctest: +NORMALIZE_WHITESPACE {'coord': array([[0., 0., 0.], [0., 0., 1.], [0., 1., 0.], [0., 1., 1.], [1., 0., 0.], [1., 0., 1.]])} Notice how attributes as list are automatically converted to numpy arrays. To delete an attribute is similar with the parent class: .. doctest:: >>> faceSet.delete_attribute("coord", "verts") Adding attribute to faces and edges are also similar: .. doctest:: >>> face_attribute = [10, 20, 30] >>> faceSet.set_attribute("area", "faces", face_attribute) >>> faceSet.face_attributes {'area': array([10., 20., 30.])} Getting attributes relating to a single cell ------------------------------------------------------- In IndexedFaceSet, vertices and their indices coincide. Therefore for all cells, the possibility has been created to ask for the attribute of a cell using the cell itself or their indices: .. doctest:: >>> faceSet.set_attribute("coord", "verts", vertex_attr) >>> faceSet.get_attribute("coord", "verts", 3) array([0., 1., 1.]) >>> faceSet.get_attribute("area", "faces", 2) 30.0 >>> faceSet.get_attribute("area", "faces", (1, 5, 3, 0)) 30.0