"""
Geometry Primitives
===================
Define various geometry primitives and their generation methods for
colour science visualizations and computations.
- :attr:`colour.geometry.MAPPING_PLANE_TO_AXIS`
- :func:`colour.geometry.primitive_grid`
- :func:`colour.geometry.primitive_cube`
- :func:`colour.PRIMITIVE_METHODS`
- :func:`colour.primitive`
References
----------
- :cite:`Cabello2015` : Cabello, R. (n.d.). PlaneGeometry.js. Retrieved May
12, 2015, from
https://github.com/mrdoob/three.js/blob/dev/src/geometries/PlaneGeometry.js
"""
from __future__ import annotations
import typing
import numpy as np
from colour.constants import DTYPE_FLOAT_DEFAULT, DTYPE_INT_DEFAULT
if typing.TYPE_CHECKING:
from colour.hints import (
Any,
DTypeFloat,
DTypeInt,
Literal,
NDArray,
Tuple,
Type,
)
from colour.hints import NDArrayFloat, cast
from colour.utilities import (
CanonicalMapping,
as_int_array,
filter_kwargs,
ones,
optional,
validate_method,
zeros,
)
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"MAPPING_PLANE_TO_AXIS",
"primitive_grid",
"primitive_cube",
"PRIMITIVE_METHODS",
"primitive",
]
MAPPING_PLANE_TO_AXIS: CanonicalMapping = CanonicalMapping(
{
"yz": "+x",
"zy": "-x",
"xz": "+y",
"zx": "-y",
"xy": "+z",
"yx": "-z",
}
)
MAPPING_PLANE_TO_AXIS.__doc__ = """
Mapping from coordinate planes to their perpendicular axes.
Maps two-letter plane identifiers (e.g., 'xy', 'yz') to their
corresponding perpendicular axis with sign indicating the direction
following the right-hand rule convention.
"""
[docs]
def primitive_grid(
width: float = 1,
height: float = 1,
width_segments: int = 1,
height_segments: int = 1,
axis: Literal[
"-x", "+x", "-y", "+y", "-z", "+z", "xy", "xz", "yz", "yx", "zx", "zy"
] = "+z",
dtype_vertices: Type[DTypeFloat] | None = None,
dtype_indexes: Type[DTypeInt] | None = None,
) -> Tuple[NDArray, NDArray, NDArray]:
"""
Generate vertices and indexes for a filled and outlined grid
primitive.
Parameters
----------
width
Width of the primitive.
height
Height of the primitive.
width_segments
Number of segments along the width.
height_segments
Number of segments along the height.
axis
Axis to which the primitive will be normal, or plane with
which the primitive will be co-planar.
dtype_vertices
:class:`numpy.dtype` to use for the grid vertices. Defaults
to the :class:`numpy.dtype` defined by the
:attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
dtype_indexes
:class:`numpy.dtype` to use for the grid indexes. Defaults
to the :class:`numpy.dtype` defined by the
:attr:`colour.constant.DTYPE_INT_DEFAULT` attribute.
Returns
-------
:class:`tuple`
Tuple of grid vertices, face indexes to produce a filled
grid, and outline indexes to produce an outline of the grid
faces.
References
----------
:cite:`Cabello2015`
Examples
--------
>>> vertices, faces, outline = primitive_grid()
>>> print(vertices)
[([-0.5, 0.5, 0. ], [ 0., 1.], [ 0., 0., 1.], [ 0., 1., 0., 1.])
([ 0.5, 0.5, 0. ], [ 1., 1.], [ 0., 0., 1.], [ 1., 1., 0., 1.])
([-0.5, -0.5, 0. ], [ 0., 0.], [ 0., 0., 1.], [ 0., 0., 0., 1.])
([ 0.5, -0.5, 0. ], [ 1., 0.], [ 0., 0., 1.], [ 1., 0., 0., 1.])]
>>> print(faces)
[[0 2 1]
[2 3 1]]
>>> print(outline)
[[0 2]
[2 3]
[3 1]
[1 0]]
"""
axis = MAPPING_PLANE_TO_AXIS.get(axis, axis).lower()
dtype_vertices = optional(dtype_vertices, DTYPE_FLOAT_DEFAULT)
dtype_indexes = optional(dtype_indexes, DTYPE_INT_DEFAULT)
x_grid = width_segments
y_grid = height_segments
x_grid1 = int(x_grid + 1)
y_grid1 = int(y_grid + 1)
# Positions, normals and uvs.
positions = zeros(x_grid1 * y_grid1 * 3)
normals = zeros(x_grid1 * y_grid1 * 3)
uvs = zeros(x_grid1 * y_grid1 * 2)
y = np.arange(y_grid1) * height / y_grid - height / 2
x = np.arange(x_grid1) * width / x_grid - width / 2
positions[::3] = np.tile(x, y_grid1)
positions[1::3] = -np.repeat(y, x_grid1)
normals[2::3] = 1
uvs[::2] = np.tile(np.arange(x_grid1) / x_grid, y_grid1)
uvs[1::2] = np.repeat(1 - np.arange(y_grid1) / y_grid, x_grid1)
# Faces and outline.
faces_indexes = []
outline_indexes = []
for i_y in range(y_grid):
for i_x in range(x_grid):
a = i_x + x_grid1 * i_y
b = i_x + x_grid1 * (i_y + 1)
c = (i_x + 1) + x_grid1 * (i_y + 1)
d = (i_x + 1) + x_grid1 * i_y
faces_indexes.extend([(a, b, d), (b, c, d)])
outline_indexes.extend([(a, b), (b, c), (c, d), (d, a)])
faces = np.reshape(as_int_array(faces_indexes, dtype_indexes), (-1, 3))
outline = np.reshape(as_int_array(outline_indexes, dtype_indexes), (-1, 2))
positions = np.reshape(positions, (-1, 3))
uvs = np.reshape(uvs, (-1, 2))
normals = np.reshape(normals, (-1, 3))
if axis in ("-x", "+x"):
shift, zero_axis = 1, 0
elif axis in ("-y", "+y"):
shift, zero_axis = -1, 1
elif axis in ("-z", "+z"):
shift, zero_axis = 0, 2
sign = -1 if "-" in axis else 1
positions = np.roll(positions, shift, -1)
normals = cast("NDArrayFloat", np.roll(normals, shift, -1)) * sign
vertex_colours = np.ravel(positions)
vertex_colours = np.hstack(
[
np.reshape(
np.interp(
cast("NDArrayFloat", vertex_colours),
(np.min(vertex_colours), np.max(vertex_colours)),
(0, 1),
),
positions.shape,
),
ones((positions.shape[0], 1)),
]
)
vertex_colours[..., zero_axis] = 0
vertices = zeros(
positions.shape[0],
[
("position", dtype_vertices, 3),
("uv", dtype_vertices, 2),
("normal", dtype_vertices, 3),
("colour", dtype_vertices, 4),
], # pyright: ignore
)
vertices["position"] = positions
vertices["uv"] = uvs
vertices["normal"] = normals
vertices["colour"] = vertex_colours
return vertices, faces, outline
[docs]
def primitive_cube(
width: float = 1,
height: float = 1,
depth: float = 1,
width_segments: int = 1,
height_segments: int = 1,
depth_segments: int = 1,
planes: (
Literal[
"-x",
"+x",
"-y",
"+y",
"-z",
"+z",
"xy",
"xz",
"yz",
"yx",
"zx",
"zy",
]
| None
) = None,
dtype_vertices: Type[DTypeFloat] | None = None,
dtype_indexes: Type[DTypeInt] | None = None,
) -> Tuple[NDArray, NDArray, NDArray]:
"""
Generate vertices and indexes for a filled and outlined cube primitive.
Parameters
----------
width
Cube width.
height
Cube height.
depth
Cube depth.
width_segments
Cube segments count along the width.
height_segments
Cube segments count along the height.
depth_segments
Cube segments count along the depth.
planes
Grid primitives to include in the cube construction.
dtype_vertices
:class:`numpy.dtype` to use for the grid vertices. Defaults to
the :class:`numpy.dtype` defined by the
:attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
dtype_indexes
:class:`numpy.dtype` to use for the grid indexes. Defaults to
the :class:`numpy.dtype` defined by the
:attr:`colour.constant.DTYPE_INT_DEFAULT` attribute.
Returns
-------
:class:`tuple`
Tuple of cube vertices, face indexes to produce a filled cube and
outline indexes to produce an outline of the faces of the cube.
Examples
--------
>>> vertices, faces, outline = primitive_cube()
>>> print(vertices)
[([-0.5, 0.5, -0.5], [ 0., 1.], [-0., -0., -1.], [ 0., 1., 0., 1.])
([ 0.5, 0.5, -0.5], [ 1., 1.], [-0., -0., -1.], [ 1., 1., 0., 1.])
([-0.5, -0.5, -0.5], [ 0., 0.], [-0., -0., -1.], [ 0., 0., 0., 1.])
([ 0.5, -0.5, -0.5], [ 1., 0.], [-0., -0., -1.], [ 1., 0., 0., 1.])
([-0.5, 0.5, 0.5], [ 0., 1.], [ 0., 0., 1.], [ 0., 1., 1., 1.])
([ 0.5, 0.5, 0.5], [ 1., 1.], [ 0., 0., 1.], [ 1., 1., 1., 1.])
([-0.5, -0.5, 0.5], [ 0., 0.], [ 0., 0., 1.], [ 0., 0., 1., 1.])
([ 0.5, -0.5, 0.5], [ 1., 0.], [ 0., 0., 1.], [ 1., 0., 1., 1.])
([ 0.5, -0.5, -0.5], [ 0., 1.], [-0., -1., -0.], [ 1., 0., 0., 1.])
([ 0.5, -0.5, 0.5], [ 1., 1.], [-0., -1., -0.], [ 1., 0., 1., 1.])
([-0.5, -0.5, -0.5], [ 0., 0.], [-0., -1., -0.], [ 0., 0., 0., 1.])
([-0.5, -0.5, 0.5], [ 1., 0.], [-0., -1., -0.], [ 0., 0., 1., 1.])
([ 0.5, 0.5, -0.5], [ 0., 1.], [ 0., 1., 0.], [ 1., 1., 0., 1.])
([ 0.5, 0.5, 0.5], [ 1., 1.], [ 0., 1., 0.], [ 1., 1., 1., 1.])
([-0.5, 0.5, -0.5], [ 0., 0.], [ 0., 1., 0.], [ 0., 1., 0., 1.])
([-0.5, 0.5, 0.5], [ 1., 0.], [ 0., 1., 0.], [ 0., 1., 1., 1.])
([-0.5, -0.5, 0.5], [ 0., 1.], [-1., -0., -0.], [ 0., 0., 1., 1.])
([-0.5, 0.5, 0.5], [ 1., 1.], [-1., -0., -0.], [ 0., 1., 1., 1.])
([-0.5, -0.5, -0.5], [ 0., 0.], [-1., -0., -0.], [ 0., 0., 0., 1.])
([-0.5, 0.5, -0.5], [ 1., 0.], [-1., -0., -0.], [ 0., 1., 0., 1.])
([ 0.5, -0.5, 0.5], [ 0., 1.], [ 1., 0., 0.], [ 1., 0., 1., 1.])
([ 0.5, 0.5, 0.5], [ 1., 1.], [ 1., 0., 0.], [ 1., 1., 1., 1.])
([ 0.5, -0.5, -0.5], [ 0., 0.], [ 1., 0., 0.], [ 1., 0., 0., 1.])
([ 0.5, 0.5, -0.5], [ 1., 0.], [ 1., 0., 0.], [ 1., 1., 0., 1.])]
>>> print(faces)
[[ 1 2 0]
[ 1 3 2]
[ 4 6 5]
[ 6 7 5]
[ 9 10 8]
[ 9 11 10]
[12 14 13]
[14 15 13]
[17 18 16]
[17 19 18]
[20 22 21]
[22 23 21]]
>>> print(outline)
[[ 0 2]
[ 2 3]
[ 3 1]
[ 1 0]
[ 4 6]
[ 6 7]
[ 7 5]
[ 5 4]
[ 8 10]
[10 11]
[11 9]
[ 9 8]
[12 14]
[14 15]
[15 13]
[13 12]
[16 18]
[18 19]
[19 17]
[17 16]
[20 22]
[22 23]
[23 21]
[21 20]]
"""
axis = (
sorted(MAPPING_PLANE_TO_AXIS.values())
if planes is None
else [MAPPING_PLANE_TO_AXIS.get(plane, plane).lower() for plane in planes]
)
dtype_vertices = optional(dtype_vertices, DTYPE_FLOAT_DEFAULT)
dtype_indexes = optional(dtype_indexes, DTYPE_INT_DEFAULT)
w_s, h_s, d_s = width_segments, height_segments, depth_segments
planes_p = []
if "-z" in axis:
planes_p.append(list(primitive_grid(width, depth, w_s, d_s, "-z")))
planes_p[-1][0]["position"][..., 2] -= height / 2
planes_p[-1][1] = np.fliplr(planes_p[-1][1])
if "+z" in axis:
planes_p.append(list(primitive_grid(width, depth, w_s, d_s, "+z")))
planes_p[-1][0]["position"][..., 2] += height / 2
if "-y" in axis:
planes_p.append(list(primitive_grid(height, width, h_s, w_s, "-y")))
planes_p[-1][0]["position"][..., 1] -= depth / 2
planes_p[-1][1] = np.fliplr(planes_p[-1][1])
if "+y" in axis:
planes_p.append(list(primitive_grid(height, width, h_s, w_s, "+y")))
planes_p[-1][0]["position"][..., 1] += depth / 2
if "-x" in axis:
planes_p.append(list(primitive_grid(depth, height, d_s, h_s, "-x")))
planes_p[-1][0]["position"][..., 0] -= width / 2
planes_p[-1][1] = np.fliplr(planes_p[-1][1])
if "+x" in axis:
planes_p.append(list(primitive_grid(depth, height, d_s, h_s, "+x")))
planes_p[-1][0]["position"][..., 0] += width / 2
positions = zeros((0, 3))
uvs = zeros((0, 2))
normals = zeros((0, 3))
faces = zeros((0, 3), dtype=dtype_indexes)
outline = zeros((0, 2), dtype=dtype_indexes)
offset = 0
for vertices_p, faces_p, outline_p in planes_p:
positions = np.vstack([positions, vertices_p["position"]])
uvs = np.vstack([uvs, vertices_p["uv"]])
normals = np.vstack([normals, vertices_p["normal"]])
faces = np.vstack([faces, faces_p + offset])
outline = np.vstack([outline, outline_p + offset])
offset += vertices_p["position"].shape[0]
vertices = zeros(
positions.shape[0],
[
("position", dtype_vertices, 3),
("uv", dtype_vertices, 2),
("normal", dtype_vertices, 3),
("colour", dtype_vertices, 4),
], # pyright: ignore
)
vertex_colours = np.ravel(positions)
vertex_colours = np.hstack(
[
np.reshape(
np.interp(
cast("NDArrayFloat", vertex_colours),
(np.min(vertex_colours), np.max(vertex_colours)),
(0, 1),
),
positions.shape,
),
ones((positions.shape[0], 1)),
]
)
vertices["position"] = positions
vertices["uv"] = uvs
vertices["normal"] = normals
vertices["colour"] = vertex_colours
return vertices, faces, outline
PRIMITIVE_METHODS: CanonicalMapping = CanonicalMapping(
{
"Grid": primitive_grid,
"Cube": primitive_cube,
}
)
PRIMITIVE_METHODS.__doc__ = """
Supported geometry primitive generation methods.
"""
[docs]
def primitive(
method: Literal["Cube", "Grid"] | str = "Cube", **kwargs: Any
) -> Tuple[NDArray, NDArray, NDArray]:
"""
Generate a geometry primitive.
This function creates geometric primitives such as cubes or grids with
configurable dimensions and segmentation. The generated primitive includes
vertices with position, texture coordinates, normal vectors, and colour
data, along with face and outline indexes for rendering.
Parameters
----------
method
Generation method for the primitive. Supported methods are:
- ``'Cube'``: Generate a 3D cube primitive
- ``'Grid'``: Generate a 2D grid primitive
Other Parameters
----------------
axis
{:func:`colour.geometry.primitive_grid`},
Axis to which the primitive will be normal, or plane with which the
primitive will be co-planar.
depth
{:func:`colour.geometry.primitive_cube`},
Cube depth.
depth_segments
{:func:`colour.geometry.primitive_cube`},
Cube segments count along the depth.
dtype_indexes
{:func:`colour.geometry.primitive_grid`,
:func:`colour.geometry.primitive_cube`},
:class:`numpy.dtype` to use for the grid indexes. Defaults to the
:class:`numpy.dtype` defined by the
:attr:`colour.constant.DTYPE_INT_DEFAULT` attribute.
dtype_vertices
{:func:`colour.geometry.primitive_grid`,
:func:`colour.geometry.primitive_cube`},
:class:`numpy.dtype` to use for the grid vertices. Defaults to the
:class:`numpy.dtype` defined by the
:attr:`colour.constant.DTYPE_FLOAT_DEFAULT` attribute.
height
{:func:`colour.geometry.primitive_grid`,
:func:`colour.geometry.primitive_cube`},
Height of the primitive.
planes
{:func:`colour.geometry.primitive_cube`},
Grid primitives to include in the cube construction.
width
{:func:`colour.geometry.primitive_grid`,
:func:`colour.geometry.primitive_cube`},
Width of the primitive.
width_segments
{:func:`colour.geometry.primitive_grid`,
:func:`colour.geometry.primitive_cube`},
Number of segments along the width.
height_segments
{:func:`colour.geometry.primitive_grid`,
:func:`colour.geometry.primitive_cube`},
Number of segments along the height.
Returns
-------
:class:`tuple`
Tuple containing three arrays:
- **vertices**: Structured array of vertex data including position,
texture coordinates, normal vectors, and colour values
- **faces**: Face indexes for rendering a filled primitive
- **outline**: Outline indexes for rendering the edges of the primitive
References
----------
:cite:`Cabello2015`
Examples
--------
>>> vertices, faces, outline = primitive()
>>> print(vertices)
[([-0.5, 0.5, -0.5], [ 0., 1.], [-0., -0., -1.], [ 0., 1., 0., 1.])
([ 0.5, 0.5, -0.5], [ 1., 1.], [-0., -0., -1.], [ 1., 1., 0., 1.])
([-0.5, -0.5, -0.5], [ 0., 0.], [-0., -0., -1.], [ 0., 0., 0., 1.])
([ 0.5, -0.5, -0.5], [ 1., 0.], [-0., -0., -1.], [ 1., 0., 0., 1.])
([-0.5, 0.5, 0.5], [ 0., 1.], [ 0., 0., 1.], [ 0., 1., 1., 1.])
([ 0.5, 0.5, 0.5], [ 1., 1.], [ 0., 0., 1.], [ 1., 1., 1., 1.])
([-0.5, -0.5, 0.5], [ 0., 0.], [ 0., 0., 1.], [ 0., 0., 1., 1.])
([ 0.5, -0.5, 0.5], [ 1., 0.], [ 0., 0., 1.], [ 1., 0., 1., 1.])
([ 0.5, -0.5, -0.5], [ 0., 1.], [-0., -1., -0.], [ 1., 0., 0., 1.])
([ 0.5, -0.5, 0.5], [ 1., 1.], [-0., -1., -0.], [ 1., 0., 1., 1.])
([-0.5, -0.5, -0.5], [ 0., 0.], [-0., -1., -0.], [ 0., 0., 0., 1.])
([-0.5, -0.5, 0.5], [ 1., 0.], [-0., -1., -0.], [ 0., 0., 1., 1.])
([ 0.5, 0.5, -0.5], [ 0., 1.], [ 0., 1., 0.], [ 1., 1., 0., 1.])
([ 0.5, 0.5, 0.5], [ 1., 1.], [ 0., 1., 0.], [ 1., 1., 1., 1.])
([-0.5, 0.5, -0.5], [ 0., 0.], [ 0., 1., 0.], [ 0., 1., 0., 1.])
([-0.5, 0.5, 0.5], [ 1., 0.], [ 0., 1., 0.], [ 0., 1., 1., 1.])
([-0.5, -0.5, 0.5], [ 0., 1.], [-1., -0., -0.], [ 0., 0., 1., 1.])
([-0.5, 0.5, 0.5], [ 1., 1.], [-1., -0., -0.], [ 0., 1., 1., 1.])
([-0.5, -0.5, -0.5], [ 0., 0.], [-1., -0., -0.], [ 0., 0., 0., 1.])
([-0.5, 0.5, -0.5], [ 1., 0.], [-1., -0., -0.], [ 0., 1., 0., 1.])
([ 0.5, -0.5, 0.5], [ 0., 1.], [ 1., 0., 0.], [ 1., 0., 1., 1.])
([ 0.5, 0.5, 0.5], [ 1., 1.], [ 1., 0., 0.], [ 1., 1., 1., 1.])
([ 0.5, -0.5, -0.5], [ 0., 0.], [ 1., 0., 0.], [ 1., 0., 0., 1.])
([ 0.5, 0.5, -0.5], [ 1., 0.], [ 1., 0., 0.], [ 1., 1., 0., 1.])]
>>> print(faces)
[[ 1 2 0]
[ 1 3 2]
[ 4 6 5]
[ 6 7 5]
[ 9 10 8]
[ 9 11 10]
[12 14 13]
[14 15 13]
[17 18 16]
[17 19 18]
[20 22 21]
[22 23 21]]
>>> print(outline)
[[ 0 2]
[ 2 3]
[ 3 1]
[ 1 0]
[ 4 6]
[ 6 7]
[ 7 5]
[ 5 4]
[ 8 10]
[10 11]
[11 9]
[ 9 8]
[12 14]
[14 15]
[15 13]
[13 12]
[16 18]
[18 19]
[19 17]
[17 16]
[20 22]
[22 23]
[23 21]
[21 20]]
>>> vertices, faces, outline = primitive("Grid")
>>> print(vertices)
[([-0.5, 0.5, 0. ], [ 0., 1.], [ 0., 0., 1.], [ 0., 1., 0., 1.])
([ 0.5, 0.5, 0. ], [ 1., 1.], [ 0., 0., 1.], [ 1., 1., 0., 1.])
([-0.5, -0.5, 0. ], [ 0., 0.], [ 0., 0., 1.], [ 0., 0., 0., 1.])
([ 0.5, -0.5, 0. ], [ 1., 0.], [ 0., 0., 1.], [ 1., 0., 0., 1.])]
>>> print(faces)
[[0 2 1]
[2 3 1]]
>>> print(outline)
[[0 2]
[2 3]
[3 1]
[1 0]]
"""
method = validate_method(method, tuple(PRIMITIVE_METHODS))
function = PRIMITIVE_METHODS[method]
return function(**filter_kwargs(function, **kwargs))