"""
Module dedicated for basic primitives.
This module provides functions to create basic geometric primitives like
spheres, boxes, cylinders, etc. that can be used for visualization.
"""
import math
from os.path import join as pjoin
import numpy as np
from packaging.version import parse
from scipy.spatial import ConvexHull
from scipy.version import short_version
from fury.data import DATA_DIR
from fury.decorators import warn_on_args_to_kwargs
from fury.transform import cart2sphere, sphere2cart
from fury.utils import fix_winding_order
SCIPY_1_4_PLUS = parse(short_version) >= parse("1.4.0")
SPHERE_FILES = {
"symmetric362": pjoin(DATA_DIR, "evenly_distributed_sphere_362.npz"),
"symmetric642": pjoin(DATA_DIR, "evenly_distributed_sphere_642.npz"),
"symmetric724": pjoin(DATA_DIR, "evenly_distributed_sphere_724.npz"),
"repulsion724": pjoin(DATA_DIR, "repulsion724.npz"),
"repulsion100": pjoin(DATA_DIR, "repulsion100.npz"),
"repulsion200": pjoin(DATA_DIR, "repulsion200.npz"),
}
[docs]
def faces_from_sphere_vertices(vertices):
"""
Triangulate a set of vertices on the sphere.
Parameters
----------
vertices : ndarray, shape (M, 3)
XYZ coordinates of vertices on the sphere.
Returns
-------
ndarray, shape (N, 3)
Indices into vertices; forms triangular faces.
"""
hull = ConvexHull(vertices, qhull_options="Qbb Qc")
faces = np.ascontiguousarray(hull.simplices)
if len(vertices) < 2**16:
return np.asarray(faces, np.uint16)
else:
return faces
def _normalize_geom_param(param, n_centers, param_name="parameter"):
"""
Normalize a geometry parameter to an array of length n_centers.
Accepts a scalar or array-like. A scalar or single-element value is
broadcast to all centers. An array of length ``n_centers`` is returned
as-is. Any other length raises ``ValueError``.
Parameters
----------
param : float or array-like
The geometry parameter value(s).
n_centers : int
Number of primitive instances (centers).
param_name : str, optional
Name used in error messages.
Returns
-------
ndarray, shape (n_centers,)
The parameter broadcast to length ``n_centers``.
"""
arr = np.atleast_1d(np.asarray(param, dtype=np.float64)).ravel()
if arr.size == 1:
return np.full(n_centers, arr[0])
if arr.size == n_centers:
return arr
raise ValueError(
f"{param_name} must be a scalar or an array of length {n_centers} "
f"(got length {arr.size})."
)
[docs]
@warn_on_args_to_kwargs()
def repeat_primitive_function(
func, centers, *, func_args=None, directions=(1, 0, 0), colors=(1, 0, 0), scales=1
):
"""
Repeat vertices and triangles of a specific primitive function.
It could be seen as a glyph. The primitive function should generate and
return vertices and faces.
Parameters
----------
func : callable
Primitive function.
centers : ndarray, shape (N, 3)
Positions for repeated primitives.
func_args : list or None, optional
Primitive function arguments/parameters.
directions : ndarray, shape (N, 3) or tuple (3,), optional
Orientation vectors for the primitives.
colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA (for opacity) colors for the primitives.
R, G, B and A should be in the range [0, 1].
scales : ndarray, shape (N,) or (N, 3) or float or int, optional
Scaling factors for the primitives.
Returns
-------
big_vertices : ndarray
Expanded vertices at the centers positions.
big_triangles : ndarray
Expanded triangles that compose the repeated primitives.
big_colors : ndarray
Expanded colors applied to all vertices/faces.
"""
if func_args is None:
func_args = []
# Get faces
_, faces = func()
if len(func_args) == 1:
func_args = np.squeeze(np.array([func_args] * centers.shape[0]))
elif len(func_args) != centers.shape[0]:
raise OSError(
"sq_params should 1 or equal to the numbers \
of centers"
)
vertices = np.concatenate([func(i)[0] for i in func_args])
return repeat_primitive(
vertices,
faces,
centers,
directions=directions,
colors=colors,
scales=scales,
have_tiled_verts=True,
)
[docs]
@warn_on_args_to_kwargs()
def repeat_primitive(
vertices,
faces,
centers,
*,
directions=None,
colors=(1, 0, 0),
scales=1,
have_tiled_verts=False,
):
"""
Repeat vertices and triangles of a specific primitive shape.
It could be seen as a glyph.
Parameters
----------
vertices : ndarray
Vertices coordinates to duplicate at the centers positions.
faces : ndarray
Triangles that compose the shape to duplicate.
centers : ndarray, shape (N, 3)
Positions for repeated primitives.
directions : ndarray, shape (N, 3) or tuple (3,), optional
Orientation vectors for the primitives.
colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,), optional
RGB or RGBA (for opacity) colors for the primitives.
R, G, B and A should be in the range [0, 1].
scales : ndarray, shape (N,) or (N, 3) or float or int, optional
Scaling factors for the primitives.
have_tiled_verts : bool, optional
Option to control if vertices need to be duplicated or not.
Returns
-------
big_vertices : ndarray
Expanded vertices at the centers positions.
big_triangles : ndarray
Expanded triangles that compose the repeated primitives.
big_colors : ndarray
Expanded colors applied to all vertices/faces.
big_centers : ndarray
Expanded centers for all vertices/faces.
"""
# duplicated vertices if needed
if not have_tiled_verts:
vertices = np.tile(vertices, (centers.shape[0], 1))
big_vertices = vertices
# Get unit shape
unit_verts_size = vertices.shape[0] // centers.shape[0]
unit_triangles_size = faces.shape[0]
# scale them
if not isinstance(scales, np.ndarray):
scales = np.array(scales)
if scales.ndim == 1:
if scales.size == centers.shape[0]:
scales = np.repeat(scales, unit_verts_size, axis=0)
scales = scales.reshape((big_vertices.shape[0], 1))
elif scales.ndim == 2:
scales = np.repeat(scales, unit_verts_size, axis=0)
big_vertices *= scales
# update triangles
big_triangles = np.array(np.tile(faces, (centers.shape[0], 1)), dtype=np.int32)
big_triangles += np.repeat(
np.arange(0, centers.shape[0] * unit_verts_size, step=unit_verts_size),
unit_triangles_size,
axis=0,
).reshape((big_triangles.shape[0], 1))
def normalize_input(arr, *, arr_name=""):
"""
Normalize input array for colors and directions.
Parameters
----------
arr : array-like
Input array to normalize.
arr_name : str, optional
Name of the array for error messages.
Returns
-------
np.ndarray
Normalized array.
"""
if (
isinstance(arr, (tuple, list, np.ndarray))
and len(arr) in [3, 4]
and not all(isinstance(i, (list, tuple, np.ndarray)) for i in arr)
):
return np.array([arr] * centers.shape[0])
elif isinstance(arr, np.ndarray) and len(arr) == 1:
return np.repeat(arr, centers.shape[0], axis=0)
elif arr is None:
return np.array([])
elif len(arr) != len(centers):
msg = f"{arr_name} size should be 1 or "
msg += "equal to the numbers of centers"
raise OSError(msg)
else:
return np.array(arr)
# update colors
colors = normalize_input(colors, arr_name="colors")
big_colors = np.repeat(colors, unit_verts_size, axis=0)
# update orientations
directions = normalize_input(directions, arr_name="directions")
for pts, dirs in enumerate(directions):
# Normal vector of the object.
dir_abs = np.linalg.norm(dirs)
if dir_abs:
normal = np.array([1.0, 0.0, 0.0])
dirs = dirs / dir_abs
v = np.cross(normal, dirs)
c = np.dot(normal, dirs)
v1, v2, v3 = v
Vmat = np.array([[0, -v3, v2], [v3, 0, -v1], [-v2, v1, 0]])
if c == -1.0:
rotation_matrix = -np.eye(3, dtype=np.float64)
else:
h = 1 / (1 + c)
rotation_matrix = (
np.eye(3, dtype=np.float64) + Vmat + (Vmat.dot(Vmat) * h)
)
else:
rotation_matrix = np.identity(3)
big_vertices[pts * unit_verts_size : (pts + 1) * unit_verts_size] = np.dot(
rotation_matrix[:3, :3],
big_vertices[pts * unit_verts_size : (pts + 1) * unit_verts_size].T,
).T
# apply centers position
big_centers = np.repeat(centers, unit_verts_size, axis=0)
big_vertices += big_centers
return big_vertices, big_triangles, big_colors, big_centers
[docs]
def prim_square():
"""
Return vertices and triangles for a square geometry.
Returns
-------
vertices : ndarray, shape (4, 3)
Coordinates of the 4 vertices that compose the square.
triangles : ndarray, shape (2, 3)
Indices of the 2 triangles that compose the square.
"""
vertices = np.array(
[[-0.5, -0.5, 0.0], [-0.5, 0.5, 0.0], [0.5, 0.5, 0.0], [0.5, -0.5, 0.0]]
)
triangles = np.array([[0, 1, 2], [2, 3, 0]], dtype="i8")
return vertices, triangles
[docs]
def prim_box(detailed=True):
"""
Return vertices and triangles for a box geometry.
Parameters
----------
detailed : bool, optional
If True, returns 24 vertices (no shared vertices between orthogonal faces).
If False, returns 8 unique vertices.
Returns
-------
vertices : ndarray
Array of vertex coordinates.
triangles : ndarray
Array of triangle indices.
"""
if detailed:
vertices = (
np.array(
[
[-1, -1, -1],
[1, -1, -1],
[1, 1, -1],
[-1, 1, -1],
[-1, -1, 1],
[1, -1, 1],
[1, 1, 1],
[-1, 1, 1],
[-1, -1, -1],
[-1, 1, -1],
[-1, 1, 1],
[-1, -1, 1],
[1, -1, -1],
[1, 1, -1],
[1, 1, 1],
[1, -1, 1],
[-1, 1, -1],
[1, 1, -1],
[1, 1, 1],
[-1, 1, 1],
[-1, -1, -1],
[1, -1, -1],
[1, -1, 1],
[-1, -1, 1],
],
dtype=np.float32,
)
* 0.5
)
triangles = np.array(
[
[2, 1, 0],
[3, 2, 0],
[4, 5, 6],
[4, 6, 7],
[8, 10, 9],
[11, 10, 8],
[12, 13, 14],
[12, 14, 15],
[16, 17, 18],
[16, 18, 19],
[20, 21, 22],
[20, 22, 23],
],
dtype=np.uint32,
)
else:
vertices = (
np.array(
[
[-1, -1, -1],
[-1, -1, 1],
[-1, 1, -1],
[-1, 1, 1],
[1, -1, -1],
[1, -1, 1],
[1, 1, -1],
[1, 1, 1],
],
dtype=np.float32,
)
* 0.5
)
triangles = np.array(
[
[0, 6, 4],
[0, 2, 6],
[0, 3, 2],
[0, 1, 3],
[2, 7, 6],
[2, 3, 7],
[4, 6, 7],
[4, 7, 5],
[0, 4, 5],
[0, 5, 1],
[1, 5, 7],
[1, 7, 3],
],
dtype=np.uint32,
)
return vertices, triangles
[docs]
@warn_on_args_to_kwargs()
def prim_sphere(*, name="symmetric362", gen_faces=False, phi=None, theta=None):
"""
Provide vertices and triangles of the spheres.
Parameters
----------
name : str, optional
Which sphere to use, one of:
* 'symmetric362'
* 'symmetric642'
* 'symmetric724'
* 'repulsion724'
* 'repulsion100'
* 'repulsion200'
gen_faces : bool, optional
If True, triangulate a set of vertices on the sphere to get the faces.
Otherwise, load the saved faces from a file.
phi : int, optional
Number of points in the latitude direction.
theta : int, optional
Number of points in the longitude direction.
Returns
-------
vertices : ndarray
Vertices coordinates that compose the sphere.
triangles : ndarray
Triangles that compose the sphere.
Examples
--------
>>> import numpy as np
>>> from fury.primitive import prim_sphere
>>> verts, faces = prim_sphere(name='symmetric362')
>>> verts.shape == (362, 3)
True
>>> faces.shape == (720, 3)
True
"""
if phi is None or theta is None:
fname = SPHERE_FILES.get(name)
if fname is None:
raise ValueError('No sphere called "%s"' % name)
res = np.load(fname)
verts = res["vertices"].copy()
faces = faces_from_sphere_vertices(verts) if gen_faces else res["faces"]
faces = fix_winding_order(res["vertices"], faces, clockwise=True)
return verts, faces
else:
phi = phi if phi >= 3 else 3
theta = theta if theta >= 3 else 3
phi_indices, theta_indices = np.arange(0, phi), np.arange(1, theta - 1)
# phi and theta angles are same as standard physics convention
phi_angles = 2 * np.pi * phi_indices / phi
theta_angles = np.pi * theta_indices / (theta - 1)
# combinations of all phi and theta angles
mesh = np.array(np.meshgrid(phi_angles, theta_angles))
combs = mesh.T.reshape(-1, 2)
_angles = np.array([[1, 1], [0, np.pi], [np.pi / 2, -np.pi / 2]])
_points = np.array(sphere2cart(_angles[0], _angles[1], _angles[2])).T
x, y, z = sphere2cart(1, combs[:, 1:], combs[:, :1])
x = np.reshape(np.append(x, _points[:, :1]), (-1,))
y = np.reshape(np.append(y, _points[:, 1:2]), (-1,))
z = np.reshape(np.append(z, _points[:, -1:]), (-1,))
verts = np.vstack([x, y, z]).T
faces = faces_from_sphere_vertices(verts)
faces = fix_winding_order(verts, faces, clockwise=True)
return verts, faces
[docs]
def prim_superquadric(roundness=(1, 1), sphere_name="symmetric362"):
"""
Provide vertices and triangles of a superquadric.
Parameters
----------
roundness : tuple of float, optional
Parameters (Phi and Theta) that control the shape of the superquadric.
sphere_name : str, optional
Which sphere to use as a base, one of:
* 'symmetric362'
* 'symmetric642'
* 'symmetric724'
* 'repulsion724'
* 'repulsion100'
* 'repulsion200'
Returns
-------
vertices : ndarray
Vertices coordinates that compose the superquadric.
triangles : ndarray
Triangles that compose the superquadric.
Examples
--------
>>> import numpy as np
>>> from fury.primitive import prim_superquadric
>>> verts, faces = prim_superquadric(roundness=(1, 1))
>>> verts.shape == (362, 3)
True
>>> faces.shape == (720, 3)
True
"""
def _fexp(x, p):
"""
Return a different kind of exponentiation.
Parameters
----------
x : float
Input value.
p : float
Exponent value.
Returns
-------
float
Result of the exponentiation.
"""
return np.sign(x) * (np.abs(x) ** p)
sphere_verts, sphere_triangles = prim_sphere(name=sphere_name)
_, sphere_phi, sphere_theta = cart2sphere(*sphere_verts.T)
phi, theta = roundness
x = _fexp(np.sin(sphere_phi), phi) * _fexp(np.cos(sphere_theta), theta)
y = _fexp(np.sin(sphere_phi), phi) * _fexp(np.sin(sphere_theta), theta)
z = _fexp(np.cos(sphere_phi), phi)
xyz = np.vstack([x, y, z]).T
vertices = np.ascontiguousarray(xyz)
return vertices, sphere_triangles
[docs]
def prim_tetrahedron():
"""
Return vertices and triangles for a tetrahedron.
This shape has a side length of two units.
Returns
-------
vertices : ndarray, shape (4, 3)
Coordinates of the 4 vertices.
triangles : ndarray, shape (4, 3)
Indices of the 4 triangles representing the tetrahedron.
"""
pyramid_vert = np.array(
[[0.5, 0.5, 0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, 0.5]]
)
pyramid_triag = np.array([[2, 0, 1], [0, 2, 3], [0, 3, 1], [1, 3, 2]], dtype="i8")
return pyramid_vert, pyramid_triag
[docs]
def prim_icosahedron():
"""
Return vertices and triangles for an icosahedron.
Returns
-------
vertices : ndarray, shape (12, 3)
Coordinates of the 12 vertices of the icosahedron.
triangles : ndarray, shape (20, 3)
Indices of the 20 triangles representing the icosahedron.
"""
phi = (1 + math.sqrt(5)) / 2.0
icosahedron_vertices = np.array(
[
[-1.0, 0.0, phi],
[0.0, phi, 1.0],
[1.0, 0.0, phi],
[-phi, 1.0, 0.0],
[0.0, phi, -1.0],
[phi, 1.0, 0.0],
[-phi, -1.0, 0.0],
[0.0, -phi, 1.0],
[phi, -1.0, 0.0],
[-1.0, 0.0, -phi],
[0.0, -phi, -1.0],
[1.0, 0.0, -phi],
]
)
icosahedron_mesh = np.array(
[
[1, 0, 2],
[2, 5, 1],
[5, 4, 1],
[3, 1, 4],
[0, 1, 3],
[0, 6, 3],
[9, 3, 6],
[8, 2, 7],
[2, 0, 7],
[0, 7, 6],
[5, 2, 8],
[11, 5, 8],
[11, 4, 5],
[9, 11, 4],
[4, 3, 9],
[11, 10, 8],
[8, 10, 7],
[6, 7, 10],
[10, 9, 6],
[9, 10, 11],
],
dtype="i8",
)
return icosahedron_vertices, icosahedron_mesh
[docs]
def prim_rhombicuboctahedron():
"""
Return vertices and triangles for a rhombicuboctahedron.
Returns
-------
vertices : ndarray, shape (24, 3)
Coordinates of the 24 vertices of the rhombicuboctahedron.
triangles : ndarray, shape (44, 3)
Indices of the 44 triangles representing the rhombicuboctahedron.
"""
phi = (math.sqrt(2) - 1) / 2.0
vertices = np.array(
[
[0.5, phi, phi],
[0.5, phi, -phi],
[0.5, -phi, phi],
[0.5, -phi, -phi],
[phi, 0.5, phi],
[phi, 0.5, -phi],
[-phi, 0.5, phi],
[-phi, 0.5, -phi],
[phi, phi, 0.5],
[phi, -phi, 0.5],
[-phi, phi, 0.5],
[-phi, -phi, 0.5],
[-0.5, phi, phi],
[-0.5, phi, -phi],
[-0.5, -phi, phi],
[-0.5, -phi, -phi],
[phi, -0.5, phi],
[phi, -0.5, -phi],
[-phi, -0.5, phi],
[-phi, -0.5, -phi],
[phi, phi, -0.5],
[phi, -phi, -0.5],
[-phi, phi, -0.5],
[-phi, -phi, -0.5],
]
)
triangles = np.array(
[
[0, 1, 2],
[1, 3, 2],
[0, 4, 5],
[0, 5, 1],
[6, 4, 7],
[4, 5, 7],
[0, 8, 4],
[0, 2, 8],
[2, 9, 8],
[8, 9, 10],
[9, 11, 10],
[6, 8, 10],
[6, 8, 4],
[6, 10, 12],
[6, 12, 7],
[7, 12, 13],
[10, 11, 14],
[10, 14, 12],
[12, 14, 15],
[12, 15, 13],
[2, 3, 16],
[3, 17, 16],
[2, 16, 9],
[9, 16, 11],
[11, 16, 18],
[18, 16, 19],
[16, 17, 19],
[11, 18, 14],
[14, 18, 19],
[14, 19, 15],
[1, 21, 3],
[1, 20, 21],
[3, 21, 17],
[17, 21, 23],
[17, 23, 19],
[21, 20, 23],
[23, 20, 22],
[19, 23, 15],
[15, 23, 13],
[13, 23, 22],
[13, 22, 7],
[22, 7, 5],
[22, 20, 5],
[20, 1, 5],
],
dtype="i8",
)
triangles = fix_winding_order(vertices, triangles, clockwise=True)
return vertices, triangles
[docs]
def prim_star(*, dim=2):
"""
Return vertices and triangles for a 5-pointed star (2D or 3D).
Parameters
----------
dim : int, optional
Dimension of the star, either 2 or 3.
Returns
-------
vertices : ndarray
Vertices coordinates that compose the star.
triangles : ndarray
Triangles that compose the star.
"""
outer_radius = 1 / 2
inner_radius = 1 / 5
z_height = 1 / 10
if dim not in (2, 3):
raise ValueError("prim_star supports only dim=2 or dim=3")
pi = math.pi
angles = [pi / 2 + i * 2 * pi / 5 for i in range(5)]
inner_angles = [ang + pi / 5 for ang in angles]
base = []
for i in range(5):
a = angles[i]
ia = inner_angles[i]
base.append([outer_radius * math.cos(a), outer_radius * math.sin(a), 0])
base.append([inner_radius * math.cos(ia), inner_radius * math.sin(ia), 0])
faces = [
[0, 1, 9],
[1, 2, 3],
[3, 4, 5],
[5, 6, 7],
[7, 8, 9],
[1, 3, 5],
[1, 5, 7],
[1, 7, 9],
]
if dim == 2:
vertices = np.array(base, dtype=float)
return vertices, np.array(faces, dtype=int)
vertices = base + [[0, 0, z_height], [0, 0, -z_height]]
top_idx, bot_idx = 10, 11
for i in range(10):
j = (i + 1) % 10
faces.append([i, j, top_idx])
faces.append([j, i, bot_idx])
vertices = np.array(vertices, dtype=float)
return vertices, np.array(faces, dtype=int)
[docs]
def prim_triangularprism():
"""
Return vertices and triangle for a regular triangular prism.
Returns
-------
vertices : ndarray
Vertices coords that compose our prism.
triangles : ndarray
Triangles that compose our prism.
"""
# Local variable to represent the square root of three rounded
# to 7 decimal places
three = float(f"{math.sqrt(3):.7f}")
vertices = np.array(
[
[0, -1 / three, 1 / 2],
[-1 / 2, 1 / 2 / three, 1 / 2],
[1 / 2, 1 / 2 / three, 1 / 2],
[-1 / 2, 1 / 2 / three, -1 / 2],
[1 / 2, 1 / 2 / three, -1 / 2],
[0, -1 / three, -1 / 2],
]
)
triangles = np.array(
[
[0, 1, 2],
[2, 1, 3],
[2, 3, 4],
[1, 0, 5],
[1, 5, 3],
[0, 2, 4],
[0, 4, 5],
[5, 4, 3],
]
)
triangles = fix_winding_order(vertices, triangles, clockwise=True)
return vertices, triangles
[docs]
def prim_pentagonalprism():
"""
Return vertices and triangles for a pentagonal prism.
Returns
-------
vertices : ndarray
Vertices coords that compose our pentagonal prism.
triangles : ndarray
Triangles that compose our pentagonal prism.
"""
# Local variable to represent the square root of five
five = math.sqrt(5)
onec = (five - 1) / 4.0
twoc = (five + 1) / 4.0
sone = (math.sqrt(10 + (2 * five))) / 4.0
stwo = (math.sqrt(10 - (2 * five))) / 4.0
vertices = np.array(
[
[stwo / 2, twoc / 2, -0.5],
[sone / 2, -onec / 2, -0.5],
[0, -1 / 2, -0.5],
[-sone / 2, -onec / 2, -0.5],
[-stwo / 2, twoc / 2, -0.5],
[stwo / 2, twoc / 2, 0.5],
[sone / 2, -onec / 2, 0.5],
[0, -1 / 2, 0.5],
[-sone / 2, -onec / 2, 0.5],
[-stwo / 2, twoc / 2, 0.5],
]
)
triangles = np.array(
[
[9, 5, 4],
[4, 5, 0],
[5, 6, 0],
[0, 6, 1],
[6, 7, 1],
[1, 7, 2],
[7, 8, 2],
[2, 8, 3],
[8, 9, 3],
[3, 9, 4],
[0, 1, 4],
[1, 4, 3],
[1, 3, 2],
[5, 6, 9],
[6, 8, 9],
[6, 7, 8],
]
)
triangles = fix_winding_order(vertices, triangles, clockwise=True)
return vertices, triangles
[docs]
def prim_octagonalprism():
"""
Return vertices and triangle for an octagonal prism.
Returns
-------
vertices : ndarray
Vertices coords that compose our octagonal prism.
triangles : ndarray
Triangles that compose our octagonal prism.
"""
# Local variable to represent the square root of two rounded
# to 7 decimal places
two = float(f"{math.sqrt(2):.7f}")
vertices = np.array(
[
[-1, -(1 + two), -1],
[1, -(1 + two), -1],
[1, (1 + two), -1],
[-1, (1 + two), -1],
[-(1 + two), -1, -1],
[(1 + two), -1, -1],
[(1 + two), 1, -1],
[-(1 + two), 1, -1],
[-1, -(1 + two), 1],
[1, -(1 + two), 1],
[1, (1 + two), 1],
[-1, (1 + two), 1],
[-(1 + two), -1, 1],
[(1 + two), -1, 1],
[(1 + two), 1, 1],
[-(1 + two), 1, 1],
]
)
triangles = np.array(
[
[0, 8, 9],
[9, 1, 0],
[5, 13, 9],
[9, 1, 5],
[3, 11, 10],
[10, 2, 3],
[2, 10, 14],
[14, 6, 2],
[5, 13, 14],
[14, 6, 5],
[7, 15, 11],
[11, 3, 7],
[7, 15, 12],
[12, 4, 7],
[0, 8, 12],
[12, 4, 0],
[0, 3, 4],
[3, 4, 7],
[0, 3, 1],
[1, 2, 3],
[2, 5, 6],
[5, 2, 1],
[8, 11, 12],
[11, 12, 15],
[8, 11, 9],
[9, 10, 11],
[10, 13, 14],
[13, 10, 9],
],
dtype="u8",
)
vertices /= 4
triangles = fix_winding_order(vertices, triangles, clockwise=True)
return vertices, triangles
[docs]
def prim_frustum():
"""
Return vertices and triangles for a square frustum prism.
Returns
-------
vertices : ndarray
Vertices coordinates that compose the frustum prism.
triangles : ndarray
Triangles that compose the frustum prism.
"""
vertices = np.array(
[
[-0.5, -0.5, 0.5],
[0.5, -0.5, 0.5],
[0.5, 0.5, 0.5],
[-0.5, 0.5, 0.5],
[-1, -1, -0.5],
[1, -1, -0.5],
[1, 1, -0.5],
[-1, 1, -0.5],
]
)
triangles = np.array(
[
[4, 6, 5],
[6, 4, 7],
[0, 2, 1],
[2, 0, 3],
[4, 3, 0],
[3, 4, 7],
[7, 2, 3],
[2, 7, 6],
[6, 1, 2],
[1, 6, 5],
[5, 0, 1],
[0, 5, 4],
],
dtype="u8",
)
vertices /= 2
triangles = fix_winding_order(vertices, triangles, clockwise=True)
return vertices, triangles
[docs]
@warn_on_args_to_kwargs()
def prim_cylinder(*, radius=0.5, height=1, sectors=36, capped=True):
"""
Return vertices and triangles for a cylinder.
Parameters
----------
radius : float, optional
Radius of the cylinder.
height : float, optional
Height of the cylinder.
sectors : int, optional
Number of sectors in the cylinder. Must be greater than 7.
capped : bool, optional
Whether the cylinder is capped at both ends or open.
Returns
-------
vertices : ndarray
Vertices coordinates that compose the cylinder.
triangles : ndarray
Triangles that compose the cylinder.
Raises
------
TypeError
If sectors is not an integer.
ValueError
If sectors is not greater than 7.
"""
if not isinstance(sectors, int):
raise TypeError("Only integers are allowed for sectors parameter")
if not sectors > 7:
raise ValueError("Sectors parameter should be greater than 7")
sector_step = 2 * math.pi / sectors
unit_circle_vertices = []
# generate a unit circle on YZ plane
for i in range(sectors + 1):
sector_angle = i * sector_step
unit_circle_vertices.append(0)
unit_circle_vertices.append(math.cos(sector_angle))
unit_circle_vertices.append(math.sin(sector_angle))
vertices = []
# generate vertices for a cylinder
for i in range(2):
h = -height / 2 + i * height
k = 0
for _ in range(sectors + 1):
uy = unit_circle_vertices[k + 1]
uz = unit_circle_vertices[k + 2]
# position vector
vertices.append(h)
vertices.append(uy * radius)
vertices.append(uz * radius)
k += 3
# base and top circle vertices
base_center_index = None
top_center_index = None
if capped:
base_center_index = int(len(vertices) / 3)
top_center_index = base_center_index + sectors + 1
for i in range(2):
h = -height / 2 + i * height
vertices.append(h)
vertices.append(0)
vertices.append(0)
k = 0
for _ in range(sectors):
uy = unit_circle_vertices[k + 1]
uz = unit_circle_vertices[k + 2]
# position vector
vertices.append(h)
vertices.append(uy * radius)
vertices.append(uz * radius)
k += 3
if capped:
vertices = np.array(vertices).reshape(2 * (sectors + 1) + 2 * sectors + 2, 3)
else:
vertices = np.array(vertices).reshape(2 * (sectors + 1), 3)
triangles = []
k1 = 0
k2 = sectors + 1
# triangles for the side surface
for _ in range(sectors):
triangles.append(k1)
triangles.append(k2)
triangles.append(k1 + 1)
triangles.append(k2)
triangles.append(k2 + 1)
triangles.append(k1 + 1)
k1 += 1
k2 += 1
if capped:
k = base_center_index + 1
for i in range(sectors):
if i < sectors - 1:
triangles.append(base_center_index)
triangles.append(k)
triangles.append(k + 1)
else:
triangles.append(base_center_index)
triangles.append(k)
triangles.append(base_center_index + 1)
k += 1
k = top_center_index + 1
for i in range(sectors):
if i < sectors - 1:
triangles.append(top_center_index)
triangles.append(k + 1)
triangles.append(k)
else:
triangles.append(top_center_index)
triangles.append(top_center_index + 1)
triangles.append(k)
k += 1
if capped:
triangles = np.array(triangles).reshape(4 * sectors, 3)
else:
triangles = np.array(triangles).reshape(2 * sectors, 3)
return vertices, triangles
[docs]
@warn_on_args_to_kwargs()
def prim_arrow(
*,
height=1.0,
resolution=10,
tip_length=0.35,
tip_radius=0.1,
shaft_radius=0.03,
):
"""
Return vertices and triangles for arrow geometry.
Parameters
----------
height : float, optional
Height of the arrow.
resolution : int, optional
Resolution of the arrow.
tip_length : float, optional
Length of the arrow tip.
tip_radius : float, optional
Radius of the arrow tip.
shaft_radius : float, optional
Radius of the arrow shaft.
Returns
-------
vertices : ndarray
Vertices coordinates of the arrow.
triangles : ndarray
Triangles that compose the arrow.
"""
shaft_height = height - tip_length
all_faces = []
shaft_outer_circle_down = []
shaft_outer_circle_up = []
tip_outer_circle = []
# calculating vertices
for i in range(resolution + 1):
x = math.cos((i * 2) * math.pi / resolution)
y = math.sin((i * 2) * math.pi / resolution)
shaft_x = x * shaft_radius
shaft_y = y * shaft_radius
tip_x = x * tip_radius
tip_y = y * tip_radius
# lower shaft circle (d)
shaft_outer_circle_down.append((0.0, shaft_x, shaft_y))
# upper shaft circle (u)
shaft_outer_circle_up.append((shaft_height, shaft_x, shaft_y))
# tip outer circle
tip_outer_circle.append((shaft_height, tip_x, tip_y))
# center, center at shaft height, center at overall height
v1, v2, v3 = (0.0, 0.0, 0.0), (shaft_height, 0.0, 0.0), (height, 0.0, 0.0)
all_verts = (
[v1, v2, v3]
+ shaft_outer_circle_down
+ shaft_outer_circle_up
+ tip_outer_circle
)
offset = len(shaft_outer_circle_down)
off_1 = 3
off_2 = off_1 + offset
off_3 = off_2 + offset
# calculating triangles
for i in range(resolution):
# down circle d[i] , 0, d[i + 1]
all_faces.append((i + off_1 + 1, i + off_1, 0))
# cylinder triangles 1 d[i], d[i + 1], u[i + 1]
all_faces.append((i + off_2 + 1, i + off_1, i + off_1 + 1))
# cylinder triangles 2 u[i + 1], u[i], d[i]
all_faces.append((i + off_1, i + off_2 + 1, i + off_2))
# tip circle u[i] , 1, d[i + 1]
all_faces.append((i + off_3 + 1, i + off_3, 1))
# tip cone t[i], t[i + 1], 2
all_faces.append((2, i + off_3, i + off_3 + 1))
vertices = np.asarray(all_verts)
triangles = np.asarray(all_faces, dtype=int)
return vertices, triangles
[docs]
def prim_cone(*, radius=0.5, height=1, sectors=10):
"""
Return vertices and triangles of a cone.
Parameters
----------
radius : float, optional
Radius of the cone.
height : float, optional
Height of the cone.
sectors : int, optional
Number of sectors in the cone. Must be greater than 2.
Returns
-------
vertices : ndarray
Vertices coordinates that compose the cone.
triangles : ndarray
Triangles that compose the cone.
Raises
------
ValueError
If sectors is less than 3.
"""
if sectors < 3:
raise ValueError("Sectors parameter should be greater than 2")
sector_angles = 2 * np.pi / sectors * np.arange(sectors)
# Circle in YZ plane
h = height / 2.0
x = np.full((sectors,), -h)
y, z = radius * np.cos(sector_angles), radius * np.sin(sector_angles)
x = np.concatenate((x, np.array([h, -h])))
y = np.concatenate((y, np.array([0, 0])))
z = np.concatenate((z, np.array([0, 0])))
vertices = np.vstack(np.array([x, y, z])).T
# index of base and top centers
base_center_index = int(len(vertices) - 1)
top_center_index = base_center_index - 1
triangles = []
for i in range(sectors):
if not i + 1 == top_center_index:
triangles.append(top_center_index)
triangles.append(i)
triangles.append(i + 1)
triangles.append(base_center_index)
triangles.append(i + 1)
triangles.append(i)
else:
triangles.append(top_center_index)
triangles.append(i)
triangles.append(0)
triangles.append(base_center_index)
triangles.append(0)
triangles.append(i)
triangles = np.array(triangles).reshape(-1, 3)
return vertices, triangles
[docs]
def prim_disk(*, radius=0.5, sectors=36):
"""
Return vertices and triangles for a disk.
Parameters
----------
radius : float, optional
Radius of the disk.
sectors : int, optional
Number of triangle sectors forming the disk.
Returns
-------
vertices : ndarray
Vertices coordinates that compose the disk.
triangles : ndarray
Triangles that compose the disk.
Raises
------
TypeError
If sectors is not an integer.
ValueError
If sectors is not greater than 7.
"""
if not isinstance(sectors, int):
raise TypeError("Only integers are allowed for sectors parameter")
if sectors <= 7:
raise ValueError("Sectors parameter should be greater than 7")
sector_step = 2 * math.pi / sectors
vertices = [(0.0, 0.0, 0.0)]
# outer circle vertices
for i in range(sectors):
angle = i * sector_step
x = math.sin(angle) * radius
y = math.cos(angle) * radius
z = 0.0
vertices.append((x, y, z))
triangles = []
for i in range(1, sectors):
triangles.append((0, i, i + 1))
triangles.append((0, sectors, 1))
return np.array(vertices, dtype=np.float32), np.array(triangles, dtype=np.int32)
[docs]
def prim_triangle():
"""
Return vertices and triangles for a triangle geometry.
Returns
-------
vertices: ndarray, shape (3, 3)
Coordinates of the 3 vertices that compose the triangle.
triangles: ndarray, shape (1, 3)
Indices of the 1 triangle that composes the geometry.
"""
vertices = np.array([[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [0.0, 0.5, 0.0]])
triangles = np.array([[0, 1, 2]], dtype="i8")
return vertices, triangles
[docs]
def prim_ring(
*, inner_radius=0.5, outer_radius=1, radial_segments=1, circumferential_segments=32
):
"""
Return vertices and triangles for a ring geometry.
Parameters
----------
inner_radius : float, optional
The inner radius of the ring (radius of the hole).
outer_radius : float, optional
The outer radius of the ring.
radial_segments : int, optional
Number of segments along the radial direction.
circumferential_segments : int, optional
Number of segments around the circumference.
Returns
-------
vertices: ndarray, shape (3, 3)
Coordinates of the 3 vertices that compose the triangle.
triangles: ndarray, shape (1, 3)
Indices of the 1 triangle that composes the geometry.
Raises
------
ValueError
If radial_segments is less than 1.
If circumferential_segments is less than 3.
If inner_radius is not between 0 and outer_radius.
"""
inner_radius = max(0, float(inner_radius))
outer_radius = max(inner_radius, float(outer_radius))
if radial_segments < 1:
raise ValueError("radial_segments must be greater than or equal to 1")
if circumferential_segments < 3:
raise ValueError("circumferential_segments must be greater than or equal to 3")
if not (0 <= inner_radius < outer_radius):
raise ValueError(
"inner_radius must be greater than equal to 0 and less than outer_radius"
)
nr = radial_segments + 1
nc = circumferential_segments
radii = np.linspace(inner_radius, outer_radius, nr, dtype=np.float32)
angles = np.linspace(0, 2 * np.pi, nc, endpoint=False, dtype=np.float32)
rr, aa = np.meshgrid(radii, angles)
rr, aa = rr.flatten(), aa.flatten()
# Convert to Cartesian coordinates (x, y, z=0)
x = rr * np.cos(aa)
y = rr * np.sin(aa)
vertices = np.column_stack([x, y, np.zeros_like(x)])
triangles = []
for i in range(nc):
for j in range(radial_segments):
v0 = i * nr + j
v1 = i * nr + (j + 1)
v2 = ((i + 1) % nc) * nr + j
v3 = ((i + 1) % nc) * nr + (j + 1)
triangles.append([v0, v1, v3])
triangles.append([v0, v3, v2])
triangles = np.array(triangles, dtype=np.uint32)
return vertices, triangles