"""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