Source code for ddg.halfedge._surface_generator

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

import ddg
import ddg.halfedge._get as get
import ddg.halfedge._modify as modify
from ddg.halfedge._surface 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() 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() 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() triangle = s.add_face(3) peak = 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() 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 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 = ddg.indexedfaceset.indexed_face_set_to_surface( ddg.indexedfaceset.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.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() stick_bot = arrow.add_face(resolution) stick_top = arrow.add_face(resolution) modify.bridge_loops(stick_bot.edge.nex, stick_top.edge) head_base = modify.extrude(stick_top) head_peak = modify.extrude(head_base) 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() 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.face_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.face_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.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