"""This module provides functions to create quad and triangle grids."""
import numpy as np
from ddg.math.parametrizations import circle, mobius_strip, torus
def _quad_faces2D(array):
"""
Create quad faces of a grid from a 2D array of grid vertex indices.
Parameters
----------
array : np.ndarray of shape (n, m)
The vertex indices of the grid.
Returns
-------
faces : np.ndarray of shape (k, 4)
Faces of the quad grid, where k is the number of faces.
Examples
--------
>>> import numpy as np
>>> from ddg.math.grids import _quad_faces2D
>>> a = np.arange(3 * 4).reshape(3, 4)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> _quad_faces2D(a)
array([[ 0, 1, 5, 4],
[ 1, 2, 6, 5],
[ 2, 3, 7, 6],
[ 4, 5, 9, 8],
[ 5, 6, 10, 9],
[ 6, 7, 11, 10]])
"""
faces = np.column_stack(
(
array[:-1, :-1].ravel(),
array[:-1, 1:].ravel(),
array[1:, 1:].ravel(),
array[1:, :-1].ravel(),
)
)
return faces
def _triangle_faces2D(array):
"""
Create triangle faces of a grid from a 2D array of grid vertex indices.
Parameters
----------
array : np.ndarray of shape (n, m)
The vertex indices of the grid.
Returns
-------
faces : np.ndarray of shape (k, 4)
Faces of the quad grid, where k is the number of faces.
Examples
--------
>>> import numpy as np
>>> from ddg.math.grids import _triangle_faces2D
>>> a = np.arange(3 * 4).reshape(3, 4)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> _triangle_faces2D(a)
array([[ 0, 1, 4],
[ 1, 2, 5],
[ 2, 3, 6],
[ 4, 5, 8],
[ 5, 6, 9],
[ 6, 7, 10],
[ 1, 5, 4],
[ 2, 6, 5],
[ 3, 7, 6],
[ 5, 9, 8],
[ 6, 10, 9],
[ 7, 11, 10]])
"""
faces = np.concatenate(
(
np.column_stack(
(
array[:-1, :-1].ravel(),
array[:-1, 1:].ravel(),
array[1:, :-1].ravel(),
)
),
np.column_stack(
(
array[:-1, 1:].ravel(),
array[1:, 1:].ravel(),
array[1:, :-1].ravel(),
)
),
)
)
return faces
def _grid3D_along_axis(array, axis):
"""
Create quad faces from a 3D index array along a given axis.
This is a helper function to create faces of a 3D grid
in different axes of the index array.
Parameters
----------
array : np.ndarray of shape (n, m, o)
The 3D vertex index array.
axis : {0, 1, 2}
Axis along which the 2D grid faces of the index array is to be created.
Returns
-------
np.ndarray of shape (k, 4)
Faces of the grid, where k is the number of faces.
Examples
--------
>>> import numpy as np
>>> from ddg.math.grids import _grid3D_along_axis
>>> index_array = np.arange(2 * 3 * 2).reshape(2, 3, 2)
>>> _grid3D_along_axis(index_array, 0)
array([[ 0, 1, 3, 2],
[ 2, 3, 5, 4],
[ 6, 7, 9, 8],
[ 8, 9, 11, 10]])
This gives you the faces parallel to the x-y-plane (if default
coordinates are used).
>>> _grid3D_along_axis(index_array, 1)
array([[ 0, 1, 7, 6],
[ 2, 3, 9, 8],
[ 4, 5, 11, 10]])
This gives you the faces parallel to the x-z-plane (if default
coordinates are used).
>>> _grid3D_along_axis(index_array, 2)
array([[ 0, 2, 8, 6],
[ 2, 4, 10, 8],
[ 1, 3, 9, 7],
[ 3, 5, 11, 9]])
This gives you the faces parallel to the y-z-plane (if default
coordinates are used).
"""
return np.vstack(
[_quad_faces2D(array.take(i, axis)) for i in range(array.shape[axis])]
)
def _quad_faces3D(array):
"""
Create the face array of a 3D grid.
Parameters
----------
array : np.ndarray of shape (n, m, o)
The vertex index array.
Returns
-------
np.ndarray of shape (k, 4)
Faces of the grid, where k is the number of faces.
Examples
--------
>>> import numpy as np
>>> from ddg.math.grids import _quad_faces3D
>>> index_array = np.arange(2 * 3 * 2).reshape(2, 3, 2)
>>> index_array
array([[[ 0, 1],
[ 2, 3],
[ 4, 5]],
<BLANKLINE>
[[ 6, 7],
[ 8, 9],
[10, 11]]])
>>> _quad_faces3D(index_array)
array([[ 0, 1, 3, 2],
[ 2, 3, 5, 4],
[ 6, 7, 9, 8],
[ 8, 9, 11, 10],
[ 0, 1, 7, 6],
[ 2, 3, 9, 8],
[ 4, 5, 11, 10],
[ 0, 2, 8, 6],
[ 2, 4, 10, 8],
[ 1, 3, 9, 7],
[ 3, 5, 11, 9]])
"""
return np.vstack([_grid3D_along_axis(array, axis) for axis in range(3)])
[docs]def quad_grid(shape):
"""
Create a rectangular grid of the input shape.
Returns the quad faces and points of a box grid in Z^2 or Z^3.
Parameters
----------
shape : tuple of length 2 or 3
Shape of the grid. The first entry defines the number of vertices
in the first coordinate axis, the second the number of vertices
in the second coordinate axid and the third (if given) the
number of vertices in the third coordinate axis.
Returns
-------
faces : np.ndarray of shape (k, 4)
The quad faces of the grid, where k is the number of faces.
coordinates : np.ndarray
Default coordinates which start at (0,0) (or (0,0,0)) and expand
to the specified values of shape.
The shape of the array is (n, 2) if the input shape has length 2,
otherwise it is (n, 3).
Raises
------
ValueError
If the given shape does not have .ndim 2 or 3.
Examples
--------
>>> from ddg.math.grids import quad_grid
>>> faces, coords = quad_grid((2, 3))
>>> faces
array([[0, 1, 3, 2],
[2, 3, 5, 4]])
>>> coords
array([[0, 0],
[1, 0],
[0, 1],
[1, 1],
[0, 2],
[1, 2]])
In the 3D case we get
>>> from ddg.math.grids import quad_grid
>>> faces, coords = quad_grid((2, 2, 2))
>>> faces
array([[0, 1, 3, 2],
[4, 5, 7, 6],
[0, 1, 5, 4],
[2, 3, 7, 6],
[0, 2, 6, 4],
[1, 3, 7, 5]])
>>> coords
array([[0, 0, 0],
[1, 0, 0],
[0, 1, 0],
[1, 1, 0],
[0, 0, 1],
[1, 0, 1],
[0, 1, 1],
[1, 1, 1]])
"""
# 2D
if len(shape) == 2:
shape = (shape[1], shape[0])
n, m = shape
grid_indices = np.arange(m * n).reshape(shape)
faces = _quad_faces2D(grid_indices)
# Create xx and yy which define a complete unit index grid
# with the shape (n, m).
xx, yy = np.mgrid[0:n, 0:m]
# Use the indices to create unit coordinates for the vertices.
coordinates = np.vstack([yy.ravel(), xx.ravel()]).T
# 3D
elif len(shape) == 3:
shape = (shape[2], shape[1], shape[0])
n, m, l = shape
grid_indices = np.arange(m * n * l).reshape(shape)
faces = _quad_faces3D(grid_indices)
# Same as the 2D case just with another dimension.
xx, yy, zz = np.mgrid[0:n, 0:m, 0:l]
coordinates = np.vstack([zz.ravel(), yy.ravel(), xx.ravel()]).T
else:
raise ValueError("Quad grid must have .ndim 2 or 3")
return faces, coordinates
[docs]def triangulate_quads(quads):
"""
Subdivide quads into lower left and upper right triangles.
Parameters
----------
quads : np.ndarray of shape (n, 4)
The quad faces of the grid, where n is the number of faces.
Returns
-------
np.ndarray of shape (n*2, 3)
The triangle faces of the given quad faces, where n*2 is the number
of triangle faces.
Examples
--------
>>> import numpy as np
>>> from ddg.math.grids import triangulate_quads
>>> quad_faces = np.array([[0, 1, 4, 3], [1, 2, 5, 4]])
>>> triangulate_quads(quad_faces)
array([[0, 1, 4],
[1, 2, 5],
[0, 4, 3],
[1, 5, 4]])
"""
lower_left_triangles = quads[:, (0, 1, 2)]
upper_right_triangles = quads[:, (0, 2, 3)]
return np.vstack((lower_left_triangles, upper_right_triangles))
[docs]def triangle_grid(shape):
r"""
Create a triangle grid of the input shape.
The input shape defines the amount of vertices in each direction.
Both 2D and 3D are supported. Returns the faces and default coordinates.
Currently, the 3D version only supports `(m, n, 1)` for the shape.
A shape of (2, 3) will return the following combinatorics ::
o-----o
/ \ /
/ \ /
o-----o
/ \ /
/ \ /
o-----o
Parameters
----------
shape : tuple of length 2 or 3
Shape of the grid. The first entry defines the number of vertices
in the first coordinate axis, the second the number of vertices
in the second coordinate axid and the third (if given) the
number of vertices in the third coordinate axis.
Returns
-------
faces : np.ndarray of shape (k, 3)
The faces of the triangle grid, where k is the number of faces.
coordinates : np.ndarray
The default coordinates which start at (0,0) (or (0,0,0)) and expand
to the specified values of shape.
The shape of the array is (n, 2) if the input shape has length 2,
otherwise it is (n, 3).
The coordinates get spanned by the basis vectors
.. math::
b_1 = (2, 0), \quad b_2 = (1, 2)
See examples for more detail.
Raises
------
ValueError
If the given shape has .ndim 3 and shape[2] != 1.
ValueError
If the given shape does not have .ndim 2 or 3.
Notes
-----
Currently supports planar 3D grids only. Meaning you can only put in a shape of
(m, n, 1) or use (m, n) for 2D grids.
Examples
--------
>>> from ddg.math.grids import triangle_grid
>>> faces, coords = triangle_grid((2, 3))
>>> faces
array([[0, 1, 2],
[2, 3, 4],
[1, 3, 2],
[3, 5, 4]])
>>> coords
array([[0, 0],
[2, 0],
[1, 2],
[3, 2],
[2, 4],
[4, 4]])
In the 3D case we get
>>> faces, coords = triangle_grid((2, 3, 1))
>>> faces
array([[0, 1, 2],
[2, 3, 4],
[1, 3, 2],
[3, 5, 4]])
>>> coords
array([[0, 0, 0],
[2, 0, 0],
[1, 2, 0],
[3, 2, 0],
[2, 4, 0],
[4, 4, 0]])
"""
if len(shape) == 3 and shape[2] != 1:
raise ValueError(
"Only supports planar grid in 3D space as of now. "
+ "Use shape of (m, n, 1) or (m, n)."
)
if len(shape) == 2:
m, n = shape[1], shape[0]
grid_indices = np.arange(m * n).reshape((m, n))
faces = _triangle_faces2D(grid_indices)
xx, yy = np.mgrid[0:m, 0:n]
A = np.vstack([yy.ravel(), xx.ravel()]).T
# B is a matrix of basis vectors for transforming A, see docstring
B = np.array(((2, 1), (0, 2)))
# Apply basis transformation on the unit coordinates A
coordinates = A @ B.T
elif len(shape) == 3:
m, n, k = shape[2], shape[1], shape[0]
grid_indices = np.arange(m * n * k).reshape(n, k)
faces = _triangle_faces2D(grid_indices)
xx, yy, zz = np.mgrid[0:m, 0:n, 0:k]
A = np.vstack([zz.ravel(), yy.ravel(), xx.ravel()]).T
# B is a matrix of basis vectors for transforming A, see docstring
B = np.array(((2, 1, 0), (0, 2, 0), (0, 0, 1)))
# Apply basis transformation on the unit coordinates A
coordinates = A @ B.T
else:
raise ValueError("Triangle grid must have .ndim 2 or 3")
return faces, coordinates
[docs]def triangulated_quad_grid(shape):
r"""
Create a triangled quad grid of the input shape.
The input shape defines the number of vertices in each direction.
Both 2D and 3D are supported. Returns the faces and default coordinates.
A shape of (2, 3) will return the following combinatorics ::
o-----o
| / |
| / |
o-----o
| / |
| / |
o-----o
Parameters
----------
shape : tuple of length 2 or 3
Shape of the grid. The first entry defines the number of vertices
in the first coordinate axis, the second the number of vertices
in the second coordinate axid and the third (if given) the
number of vertices in the third coordinate axis.
Returns
-------
faces : np.ndarray of shape (k, 3)
The faces of the triangle grid, where k is the number of faces.
coordinates : np.ndarray
The default coordinates which start at (0,0) (or (0,0,0)) and expand
to the specified values of shape.
The shape of the array is (n, 2) if the input shape has length 2,
otherwise it is (n, 3).
Examples
--------
>>> from ddg.math.grids import triangulated_quad_grid
>>> faces, coords = triangulated_quad_grid((2, 3))
>>> faces
array([[0, 1, 3],
[2, 3, 5],
[0, 3, 2],
[2, 5, 4]])
>>> coords
array([[0, 0],
[1, 0],
[0, 1],
[1, 1],
[0, 2],
[1, 2]])
In the 3D case we get
>>> faces, coords = triangulated_quad_grid((2, 2, 2))
>>> faces
array([[0, 1, 3],
[4, 5, 7],
[0, 1, 5],
[2, 3, 7],
[0, 2, 6],
[1, 3, 7],
[0, 3, 2],
[4, 7, 6],
[0, 5, 4],
[2, 7, 6],
[0, 6, 4],
[1, 7, 5]])
>>> coords
array([[0, 0, 0],
[1, 0, 0],
[0, 1, 0],
[1, 1, 0],
[0, 0, 1],
[1, 0, 1],
[0, 1, 1],
[1, 1, 1]])
"""
quads, coordinates = quad_grid(shape)
faces = triangulate_quads(quads)
return faces, coordinates
def _raw_grid_boundary_lines(shape, axis=0):
"""The boundary lines of the grid (without gluing sides) along some axis."""
if len(shape) != 2:
raise ValueError
m, n = shape
array = np.arange(m * n).reshape(n, m)
return np.column_stack((array.take(0, axis), array.take(-1, axis)))
def _faces_from_gluing_boundaries(shape, periodicity):
"""Creates the boundary lines and edges of the grid with respect to the
orientation of the lines given by periodicity. Then creates the faces
by gluing the boundary lines.
Returns
-------
np.ndarray
the boundary lines each stored in the columns
"""
horizontal_boundary_side = _raw_grid_boundary_lines(shape, axis=0)
vertical_boundary_side = _raw_grid_boundary_lines(shape, axis=1)
edges = np.vstack((horizontal_boundary_side[0, :], horizontal_boundary_side[-1, :]))
faces = []
if periodicity == (0, 0):
pass
# Cylinder x-axis
elif periodicity == (0, 1):
faces = _quad_faces2D(horizontal_boundary_side)
# Cylinder y-axis
elif periodicity == (1, 0):
faces = _quad_faces2D(vertical_boundary_side)
# Torus
elif periodicity == (1, 1):
faces = _quad_faces2D(horizontal_boundary_side)
faces = np.vstack((faces, _quad_faces2D(vertical_boundary_side)))
face_from_edges = np.concatenate((edges[0, :], edges[-1, ::-1]))
faces = np.vstack((faces, face_from_edges))
# Mobius x-axis
elif periodicity == (0, -1):
faces = _quad_faces2D(
np.column_stack(
(horizontal_boundary_side[::-1, 0], horizontal_boundary_side[:, 1])
)
) # reverse orientation of one boundary side
# Mobius y-axis
elif periodicity == (-1, 0):
faces = _quad_faces2D(
np.column_stack(
(vertical_boundary_side[::-1, 0], vertical_boundary_side[:, 1])
)
) # reverse orientation of one boundary side
elif periodicity in [(-1, -1), (1, -1), (-1, 1)]:
raise NotImplementedError(f"Periodicity {periodicity} is not implemented.")
else:
raise ValueError(f"{periodicity} is not a valid value for periodicity.")
return faces
def _quad_grid_periodicity_coordinates(shape, periodicity):
"""Creates default coordinate for visualization of the grid for each
specific periodicity.
"""
if len(shape) != 2:
raise ValueError
m, n = shape
coordinates = []
_, uv_coordinates = quad_grid((m, n))
if periodicity == (0, 0):
_, coordinates = quad_grid((m, n, 1))
# Cylinder x-axis
elif periodicity == (1, 0):
t = np.linspace(0, 2 * np.pi, m, endpoint=False)
coordinates = [
circle(t[i], (0, j, 0), 1, [0, 1, 0])
for j in range(n)
for i in range(len(t))
]
# Cylinder y-axis
elif periodicity == (0, 1):
t = np.linspace(0, 2 * np.pi, n, endpoint=False)
coordinates = [
circle(t[i], (j, 0, 0), 1, [1, 0, 0])
for i in range(len(t))
for j in range(m)
]
# Torus
elif periodicity == (1, 1):
t1 = np.linspace(0, 2 * np.pi, min(m, n), endpoint=False)
t2 = np.linspace(0, 2 * np.pi, max(m, n), endpoint=False)
coordinates = [
torus(t2_, t1_, 2, 0.5) for t1_ in t1 for t2_ in t2
] # Torus with maximum vertex numbers on the large circle
# Mobius x-axis
elif periodicity == (-1, 0):
t1 = np.linspace(0, 2 * np.pi, m, endpoint=False)
t2 = np.linspace(-1, 1, n, endpoint=False)
coordinates = [mobius_strip(t1_, t2_, 1) for t2_ in t2 for t1_ in t1]
# Mobius y-axis
elif periodicity == (0, -1):
t1 = np.linspace(0, 2 * np.pi, n, endpoint=False)
t2 = np.linspace(-1, 1, m, endpoint=False)
coordinates = [mobius_strip(t1_, t2_, 1) for t1_ in t1 for t2_ in t2]
else:
raise ValueError("Wrong value of periodicity for a 2D grid.")
return np.array(coordinates), np.array(uv_coordinates)
[docs]def quad_grid_with_periodicity(shape, periodicity=(0, 0)):
"""
Create a quad grid of the input shape with a given periodicity.
A quad grid with periodicity `(0, 0)` is the box in
Z^2 with lower-left corner `(0, 0)` and upper-right corner
`(shape[0] - 1, shape[1] - 1)`. In other words, the vertices are
`(i_0, i_1)` with `0 <= i_0 <= shape[0] - 1` and `0 <= i_1 <= shape[1] - 1`.
A quad grid with periodicity not equal to `(0, 0)` is obtained from the
quad grid with periodicity `(0, 0)` by gluing the boundary edges as
described in the table below.
Parameters
----------
shape : tuple of length 2
Shape of the grid. The first entry defines the number of vertices
in the first coordinate axis, the second the number of vertices
in the second coordinate axis.
periodicity : tuple of length 2 (default=(0,0))
The periodicity of the grid.
+-------------+---------------+-----------------------------------------------+
| Periodicity | Topology | Gluing Axis |
+=============+===============+===============================================+
| (0, 0) | Disk | None |
+-------------+---------------+-----------------------------------------------+
| (1, 0) | Cylinder | along the first axis |
+-------------+---------------+-----------------------------------------------+
| (0, 1) | Cylinder | along the second axis |
+-------------+---------------+-----------------------------------------------+
| (1, 1) | Torus | along both axes |
+-------------+---------------+-----------------------------------------------+
| (-1, 0) | Moebius Band | along the first axis in reversed orientation |
+-------------+---------------+-----------------------------------------------+
| (0, -1) | Moebius Band | along the second axis in reversed orientation |
+-------------+---------------+-----------------------------------------------+
Returns
-------
faces : numpy.ndarray of shape (k, 4)
The faces of the quad grid, where k is the number of faces.
coordinates : numpy.ndarray
The default coordinates. The shape of the array is (n, 2).
uv_coordinates : numpy.ndarray
Returned only if periodicity != (0, 0). Stores the corresponding
coordinates in Z^2. The shape of the array is (n, 2).
Raises
------
ValueError
If shape or periodicity doesn't have the correct length or invalid values.
See Also
--------
ddg.math.grids.quad_grid : Sub-grids of Z^n without any gluing.
Examples
--------
>>> from ddg.math.grids import quad_grid_with_periodicity
>>> faces, coords, uv_coords = quad_grid_with_periodicity((2, 3), (1, 0))
>>> faces
array([[0, 1, 3, 2],
[2, 3, 5, 4],
[0, 1, 3, 2],
[2, 3, 5, 4]])
>>> coords
array([[ 1.0000000e+00, 0.0000000e+00, 0.0000000e+00],
[-1.0000000e+00, 0.0000000e+00, 1.2246468e-16],
[ 1.0000000e+00, 1.0000000e+00, 0.0000000e+00],
[-1.0000000e+00, 1.0000000e+00, 1.2246468e-16],
[ 1.0000000e+00, 2.0000000e+00, 0.0000000e+00],
[-1.0000000e+00, 2.0000000e+00, 1.2246468e-16]])
"""
if 0 in shape:
raise ValueError(
f"{shape} is not a valid shape for a grid. None of the entries must be 0."
)
if len(shape) != 2:
raise ValueError(
"Grids are only implemented for dimension 2"
+ f", but {shape = } has != 2 entries."
)
if len(periodicity) != 2:
raise ValueError(
f"{periodicity} is not a valid periodicity, tuple must be of length 2"
)
faces, coordinates = quad_grid(shape)
if periodicity != (0, 0):
periodic_faces = _faces_from_gluing_boundaries(shape, periodicity)
faces = np.vstack((faces, periodic_faces))
coordinates, uv_coordinates = _quad_grid_periodicity_coordinates(
shape, periodicity
)
else:
uv_coordinates = coordinates
return faces, coordinates, uv_coordinates
[docs]def quad_grid_sandwich(shape):
"""
Create faces for a rectangle folded in half with overlapping boundaries identified.
Start with a rectangle of shape `(2*m-1, n)`.
Fold the rectangle along the line `(m, *)`.
Identify overlapping boundary vertices and edges.
Topologically the result is a sphere.
For a shape of `(3,3)` one obtains ::
- 7--8--9--8--7
| | | | | |
n 3--4--5--6--3
| | | | | |
- 0--1--2--1--0
|--m--|
|---2*m-1---|
where vertices with equal numbers are identified.
Parameters
----------
shape : tuple of length 2
Shape of the folded rectangle viewed from the top.
Should be of the form `(m, n)` with `m >= 2, n >= 3`.
Returns
-------
faces : np.ndarray of shape (F, 4)
The faces of the quad grid, where F is the number of faces.
uv : np.ndarray of shape (V, 2)
Integer square grid coordinates for the vertices,
where V is the number of vertices.
uv_faces : np.ndarray of shape (F, 2)
Half-integer square grid coordinates for the faces,
where F is the number of faces.
Examples
--------
>>> from ddg.math.grids import quad_grid_sandwich
>>> faces, uv, uv_faces = quad_grid_sandwich((2, 3))
>>> faces
array([[0, 1, 3, 2],
[1, 0, 2, 3],
[2, 3, 5, 4],
[3, 2, 4, 5]])
>>> uv
array([[0, 0],
[1, 0],
[0, 1],
[1, 1],
[0, 2],
[1, 2]])
>>> uv_faces
array([[1.5, 0.5],
[1.5, 0.5],
[1.5, 1.5],
[1.5, 1.5]])
"""
m, n = shape
M, N = 2 * m - 2, n - 2
faces, uv, _ = quad_grid_with_periodicity((M, N), periodicity=(1, 0))
# using a bug: in the following case the coordinates are uv coordinates
_, uv = quad_grid((M, N))
# shift faces
faces = faces + m
# create vertex indices for first seam
v1 = np.vstack(
(
np.concatenate((np.arange(m), np.arange(m - 2, -1, -1))),
np.append(np.arange(m, m + M), m),
)
)
faces_seam1 = _quad_faces2D(v1)
# create vertex indices for second seam
A = (N - 1) * M + m
B = N * M + m
v2 = np.vstack(
(
np.append(np.arange(A, A + M), A),
np.concatenate((np.arange(B, B + m), np.arange(B + m - 2, B - 1, -1))),
)
)
faces_seam2 = _quad_faces2D(v2)
# join all faces
faces = np.vstack((faces_seam1, faces, faces_seam2))
# shift coordinates
uv = np.add(uv, [0, 1])
# first seam coordinates
coords_seam1 = np.column_stack((np.arange(m), np.zeros(m, int)))
# second seam coordinates
coords_seam2 = np.column_stack((np.arange(m), np.array([n - 1] * m)))
# join all coordinates
uv = np.vstack((coords_seam1, uv, coords_seam2))
# uv coordinates on faces (half integer coordinates)
# not a nice way to compute these value, but it works
uv_faces = []
for f in faces:
max_value = np.max([uv[i] for i in f], axis=0)
if max_value[0] == M - 1 and np.min([uv[i] for i in f], axis=0)[0] == 0:
uv_faces.append(max_value + [0.5, -0.5])
else:
uv_faces.append(max_value - [0.5, 0.5])
uv_faces = np.array(uv_faces)
return faces, uv, uv_faces