Source code for ddg.datastructures.halfedge.surface_generator

"""Module 'surface_generator' defines some primitive halfedge surfaces"""
import numpy as np
from scipy.spatial import ConvexHull

import ddg.datastructures.halfedge.get as get
import ddg.datastructures.halfedge.modify as modify
import ddg.datastructures.indexedfaceset as ifs
import ddg.math.euclidean
from ddg.datastructures.halfedge import surface

#######
# grids
#######


[docs]def grid(m, n, generate_coordinates=True): """ Create a quadrilateral grid with m x n quadrilaterals. By default, standard coordinates will be assigned to the vertices and can be accessed via the attribute coordinate `co`. Parameters ---------- m : int Number of quadrilaterals in first direction. n : int Number of quadrilaterals in second direction. generate_coordinates : bool, optional Decides on whether a container like coordinate attribute with default standard coordinates shall be assigned to the vertices. Returns ------- s : surface The created quadrilateral grid. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains some default standard coordinates. """ s = surface.Surface() verts = [] for i in range((m + 1) * (n + 1)): verts.append(s.verts()) quads = [] for j in range(n): for i in range(m): # create quad with given vertices quad = s.add_face( 4, [ verts[j * (m + 1) + i], verts[j * (m + 1) + i + 1], verts[(j + 1) * (m + 1) + i + 1], verts[(j + 1) * (m + 1) + i], ], ) quads.append(quad) v0 = list(s.verts)[0] # glue quads if i > 0: s.glue(quads[j * m + i - 1].edge.nex.nex, quads[j * m + i].edge) if j > 0: s.glue(quads[(j - 1) * m + i].edge.pre, quads[j * m + i].edge.nex) if generate_coordinates: s.verts.add_attribute("co") for j in range(n): for i in range(m): if generate_coordinates: quads[j * m + i].edge.head.co = np.array([i, j, 0]) quads[j * m + i].edge.opp.head.co = np.array([i, j + 1, 0]) quads[j * m + i].edge.nex.nex.head.co = np.array([i + 1, j + 1, 0]) quads[j * m + i].edge.nex.nex.opp.head.co = np.array([i + 1, j, 0]) return s
[docs]def triangle_grid(m, n, generate_coordinates=True, matrix=np.identity(3)): """ Create a triangle grid with m x n triangles. By default, standard coordinates will be assigned to the vertices and can be accessed via the attribute coordinate `co`. Parameters ---------- m : int Number of triangles in first direction. n : int Number of triangles in second direction. generate_coordinates : bool Decides on whether a container like coordinate attribute with default standard coordinates shall be assigned to the vertices. matrix : numpy.ndarray Standard coordinates get multiplied by this matrix. Returns ------- s : surface The created triangle grid. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains some default standard coordinates. """ s = surface.Surface() triangles = [] for j in range(n): for i in range(2 * m): triangles.append(s.add_face(3)) if generate_coordinates: s.verts.add_attribute("co") for j in range(n): for i in range(m): if generate_coordinates: # set coordinate just for the triangles in list with even index # the upside down ones, which are needed to fill the row, will # get their vertex coordinates through getting glued to the others bottom_edge = triangles[2 * i + j * 2 * m].edge bottom_edge.head.co = np.array([2 * i + j, 2 * j, 0]) bottom_edge.nex.head.co = np.array([2 * i + 1 + j, 2 * j + 2, 0]) bottom_edge.nex.nex.head.co = np.array([2 * i + 2 + j, 2 * j, 0]) # assure the coordinates for the last upside-down triangle in a row last_edge = triangles[2 * i + 1 + j * 2 * m].edge last_edge.nex.head.co = np.array([2 * (i + 1) + 1 + j, 2 * j + 2, 0]) if i != m - 1: # glue next upside-down triangle in on both sides s.glue( triangles[2 * i + j * 2 * m].edge.pre, triangles[2 * i + 1 + j * 2 * m].edge, ) s.glue( triangles[2 * (i + 1) + j * 2 * m].edge.nex, triangles[2 * i + 1 + j * 2 * m].edge.pre, ) else: # then last bottom-down triangle in column is reached, # then just glue the left hand side of upside down triangle s.glue( triangles[2 * i + j * 2 * m].edge.pre, triangles[2 * i + 1 + j * 2 * m].edge, ) if 0 < j < n: # nothing to glue for the first row # glue bottom of triangle in next row to bottom of upside down one s.glue( triangles[2 * i + j * 2 * m].edge, triangles[2 * i + 1 + (j - 1) * 2 * m].edge.nex, ) for v in s.verts: v.co = matrix.dot(v.co) # v.co = np.array(new_co) return s
################# # platonic solids #################
[docs]def tetrahedron(generate_coordinates=True): """ Creates a tetrahedron. By default, standard coordinates of a unit tetrahedron will be assigned to the vertices and can be accessed via the attribute coordinate `co`. Parameters ---------- generate_coordinates : bool Decides on whether a container like coordinate attribute with default standard coordinates shall be assigned to the vertices. Returns ------- s : surface The tetrahedron that has been created. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains the standard coordinates of a unit tetrahedron. """ s = surface.Surface() triangle = s.add_face(3) peak = ddg.datastructures.halfedge.modify.attach_pyramid(triangle.edge.opp) if generate_coordinates: s.verts.add_attribute("co") triangle.edge.head.co = np.array([0, 0, 0]) triangle.edge.nex.head.co = np.array([1, 1, 0]) triangle.edge.nex.nex.head.co = np.array([0, 1, 1]) peak.co = np.array([1, 0, 1]) return s
[docs]def cube(generate_coordinates=True): """ Creates a cube. By default, standard coordinates of a unit cube will be assigned to the vertices and can be accessed via the attribute coordinate `co`. Parameters ---------- generate_coordinates : bool Decides on whether a container like coordinate attribute with default standard coordinates shall be assigned to the vertices. Returns ------- c : surface The cube that has been created. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains the standard coordinates of a unit cube. """ c = surface.Surface() quads = [] for i in range(2): quads.append(c.add_face(4)) if generate_coordinates: c.verts.add_attribute("co") # front face (anti-clockwise) quads[0].edge.head.co = np.array([0.0, 0.0, 0.0]) quads[0].edge.opp.head.co = np.array([1.0, 0.0, 0.0]) quads[0].edge.nex.nex.head.co = np.array([1.0, 1.0, 0.0]) quads[0].edge.nex.nex.opp.head.co = np.array([0.0, 1.0, 0.0]) # back face, with other orientation (clockwise) quads[1].edge.head.co = np.array([0.0, 0.0, 1.0]) quads[1].edge.opp.head.co = np.array([0.0, 1.0, 1.0]) quads[1].edge.nex.nex.head.co = np.array([1.0, 1.0, 1.0]) quads[1].edge.nex.nex.opp.head.co = np.array([1.0, 0.0, 1.0]) # for brideging, take two halfedges that sit on the same side of the face, # as the edges are facing their "start"vertex with opposite orientation, # they dont lie opposite on the same side of their quads, so take the next of # one of them ddg.datastructures.halfedge.modify.bridge_loops(quads[0].edge, quads[1].edge.nex) return c
[docs]def octahedron(): """ Creates a octahedron. By default, standard coordinates of a unit octahedron, centered at [0,0,0], will be assigned to the vertices and can be accessed via the attribute coordinate `co`. Returns ------- s : surface The octahedron that has been created. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains the standard coordinates of a unit octahedron. """ octahedron = [[0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1], [1, 0, 0], [-1, 0, 0]] s = convexhull_3d(octahedron) return s
[docs]def dodecahedron(): """ Creates a dodecahedron. By default, standard coordinates of a unit dodecahedron, centered at [0, 0, 0], will be assigned to the vertices and can be accessed via the attribute coordinate `co`. Returns ------- s : surface The dodecahedron that has been created. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains the standard coordinates of a unit dodecahedron. """ p = (1 + np.sqrt(5)) / 2 dodecahedron = [ [1, 1, 1], [1, 1, -1], [1, -1, 1], [1, -1, -1], [-1, 1, 1], [-1, 1, -1], [-1, -1, 1], [-1, -1, -1], [0, p, 1 / p], [0, p, -1 / p], [0, -p, 1 / p], [0, -p, -1 / p], [1 / p, 0, p], [1 / p, 0, -p], [-1 / p, 0, p], [-1 / p, 0, -p], [p, 1 / p, 0], [p, -1 / p, 0], [-p, 1 / p, 0], [-p, -1 / p, 0], ] s = convexhull_3d(dodecahedron) return s
[docs]def icosahedron(): """ Creates a icosahedron. By default, standard coordinates of a unit icosahedron, centered at [0, 0, 0], will be assigned to the vertices and can be accessed via the attribute coordinate `co`. Returns ------- s : surface The icosahedron that has been created. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains the standard coordinates of a unit icosahedron. """ icosahedron = [] p = (1 + np.sqrt(5)) / 2 icosahedron = [ [0, 1, p], [0, 1, -p], [0, -1, p], [0, -1, -p], [p, 0, 1], [p, 0, -1], [-p, 0, 1], [-p, 0, -1], [1, p, 0], [1, -p, 0], [-1, p, 0], [-1, -p, 0], ] s = convexhull_3d(icosahedron) return s
[docs]def icosphere(subdivision_steps=1, radius=1, generate_coordinates=True): """ Creates an icosphere. By default, standard coordinates of a unit icosphere, centered at [0, 0, 0], will be assigned to the vertices and can be accessed via the attribute coordinate `co`. Parameters ---------- subdivision_steps : int Number of subdivisions performed, e.g. 0 for the icosahedron, 1 to subdivide each face of the icosahedron into 4 triangles, 2 to subdivide each face of the icosahedron into 16 triangles and so on. radius : float radius of the icosphere generate_coordinates : bool If true, generate coordinates for the icosphere, otherwise don't generate coordinates. Returns ------- s : surface The icosphere. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains the standard coordinates of a unit icosphere. """ icosphere = icosahedron() radius = radius if generate_coordinates: modify.subdivide(icosphere, subdivision_steps, "co") else: modify.subdivide(icosphere, subdivision_steps, None) for v in icosphere.verts: v.co *= radius / np.linalg.norm(v.co) return icosphere
############# # convex hull #############
[docs]def convexhull_3d(A, join_coplanar_triangles=True): """ Creates a halfedge surface of the convex hull of the input points. Parameters ---------- A : array Input array of 3 dimensional coordinates of the form [ , , ] Dimension of the input points has to be three, i.e. non planar. join_coplanar_triangles : True If set to True, planar faces will stay triangulated. Otherwise the triangulation will be removed and result in one bigger face. Returns ------- s : surface The convex hull that has been created. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains the coordinates of the input. """ hull = ConvexHull(A) s = ifs.utils.indexed_face_set_to_surface( ifs.ifs.GeneralizedIndexedFaceSet(hull.simplices) ) index_list = list(v.ifs_index for v in s.verts) # gernerating coordinates s.verts.add_attribute("co") for f in s.faces: for e in get.get_edge_loop(f.edge): i = e.head.ifs_index if i in index_list: e.head.co = np.array(A[i]) index_list.remove(i) # Check for lin independency and joins faces if needed if join_coplanar_triangles: modify.join_coplanar_faces(s) return s
####### # arrow #######
[docs]def arrow( resolution=20, generate_coordinates=True, levels=[[0, 0.05], [0.7, 0.05], [0.7, 0.125], 1], ): """ Create an arrow. By default the the bottom of the arrow is at (0,0,0), it point in the z direction, and has unit length. Parameters ---------- resolution : int (default=10) Number of vertices around the center of rotation. Minimum is 3. generate_coordinates : bool (default=True) If true, generate coordinates for the icosphere, otherwise don't generate coordinates. levels : list (default=[[0,0.05],[0.7,0.05],[0.7,0.125],1]) A 4-elements list defining the coordinates. The first three elements define the bottom, the top of the stick and the bas of the head. This three must be a 2-elements list reprensenting the height and the radius. The last element defines the height of the peak of the head. Returns ------- s : surface The arrow. Notes ----- * Coordinate attribute The coordinate attribute `co` assigned to the vertices is of type `numpy.ndarray` and contains the coordinates of the input. """ arrow = surface.Surface() stick_bot = arrow.add_face(resolution) stick_top = arrow.add_face(resolution) ddg.datastructures.halfedge.modify.bridge_loops(stick_bot.edge.nex, stick_top.edge) head_base = modify.extrude(stick_top) head_peak = modify.extrude(head_base) ddg.datastructures.halfedge.modify.contract_face(head_peak) if generate_coordinates: arrow.verts.add_attribute("co") bot_co = [ [ levels[0][1] * np.cos(i / resolution * 2 * np.pi), levels[0][1] * np.sin(i / resolution * 2 * np.pi), levels[0][0], ] for i in range(resolution) ] top_co = [ [ levels[1][1] * np.cos((resolution - i) / resolution * 2 * np.pi), levels[1][1] * np.sin((resolution - i) / resolution * 2 * np.pi), levels[1][0], ] for i in range(resolution) ] base_co = [ [ levels[2][1] * np.cos((resolution - i) / resolution * 2 * np.pi), levels[2][1] * np.sin((resolution - i) / resolution * 2 * np.pi), levels[2][0], ] for i in range(resolution) ] peak_co = [[0, 0, levels[3]]] l = bot_co + top_co + base_co + peak_co for i, v in enumerate(arrow.verts): v.co = l[i] return arrow
[docs]def cylinder( resolution=20, fill_caps=True, generate_coordinates=True, top_radius=1, bot_radius=1, length=1, co_attr="co", ): """ Creates a halfedge cylinder aligned along the y-axis. Parameters ---------- resolution: int (default=20) Number of faces around the cylinder fill_caps: bool (default=True) If `True`, faces are added at the ends of the cylinder. generate_coordinates: bool (default=True) If `True`, standard coordinates are set to `co_attr`. top_radius: float (default=1) Size of the top of the cylinder, located at (0, length, 0). bot_radius: float (default=1) Size of the bottom of the cylinder, located at (0, 0, 0). length: float (default=1) Size length of the cylinder along its axis. co_attr: str (default='co') If `generate_coordinates` is given this is the name of the vertex attribute storing the coordinates. Returns ------- The halfedge cylinder. """ s = surface.Surface() bot = s.add_face(resolution) top = s.add_face(resolution) modify.bridge_loops(bot.edge.nex, top.edge) if generate_coordinates: s.verts.add_attribute(co_attr) t = np.linspace(0, 2 * np.pi, resolution + 1)[:-1] for i, v in enumerate(get.get_vertices(bot)): setattr( v, co_attr, np.array([bot_radius * np.cos(t[i]), 0, bot_radius * np.sin(t[i])]), ) for i, v in enumerate(get.get_vertices(top)): setattr( v, co_attr, np.array( [top_radius * np.cos(t[-i]), length, top_radius * np.sin(t[-i])] ), ) if bot_radius == 0: modify.contract_face(bot) elif not fill_caps: modify.remove_face(bot) if top_radius == 0: modify.contract_face(top) elif not fill_caps: modify.remove_face(top) return s
[docs]def disc( circle_subdivisions=20, generate_coordinates=True, fill_face=True, center=None, normal=None, radius=1, co_attr="co", ): """ Create a 2-dimensional half-edge circle or disc in 3-dimensional space as a loop of half-edges. Parameters ---------- circle_subdivisions: int (default=20) Number of vertices on the boundary of the circle or disc. generate_coordinates: bool (default=True) If True, coordinates are generated and assigned to the vertices as an attribute with the given co_attr name. They depend on center, normal and radius. If False, the half-edge disc is purely combinatoric. fill_face: bool (default=True) Bool to distinguish whether to fill the edge loop with a face or not, i.e. whether to create a disc or a circle center: integrable of three floats (default=None) Center of the circle. Only used if generate_coordinates=True. None defaults to np.array([0, 0, 0]). normal: integrable of three floats (default=None) Normal of the circle. Only used if generate_coordinates=True. None defaults to np.array([0, 0, 1]). radius: float (default=1) Radius of the circle. Only used if generate_coordinates=True. co_attr: string (default='co') Name of the vertex attribute that stores the coordinates. Only used if generate_coordinates=True. Depends on center, normal and radius. Returns ------- ddg.halfedge.Surface """ normal = np.array([0, 0, 1]) if normal is None else normal center = np.array([0, 0, 0]) if center is None else center s = ddg.datastructures.halfedge.Surface() f = s.add_face(circle_subdivisions) if generate_coordinates: s.verts.add_attribute(co_attr) t = np.linspace(0, 2 * np.pi, circle_subdivisions + 1)[:-1] for v in s.verts: setattr( v, co_attr, ddg.math.euclidean.circle_fct(t[v.index], center, radius, normal), ) if not fill_face: ddg.halfedge.modify.remove_face(f) return s