Source code for colour.geometry.primitives

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