Source code for colour.geometry.primitives

"""
Geometry Primitives
===================

Define various geometry primitives and their generation methods:

-   :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 numpy as np

from colour.constants import DTYPE_FLOAT_DEFAULT, DTYPE_INT_DEFAULT
from colour.hints import (
    Any,
    DTypeFloat,
    DTypeInt,
    Literal,
    NDArray,
    NDArrayFloat,
    Tuple,
    Type,
    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__ = """Plane to axis mapping."""


[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 Grid width. height Grid height. width_segments Grid segments count along the width. height_segments Grid segments count along the height. axis Axis the primitive will be normal to, or plane the primitive will be co-planar with. dtype_vertices :class:`numpy.dtype` to use for the grid vertices, default 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, default 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 faces of the grid. 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() # pyright: ignore 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, default 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, default 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]: """ Return a geometry primitive using given method. Parameters ---------- method Generation method. Other Parameters ---------------- axis {:func:`colour.geometry.primitive_grid`}, Axis the primitive will be normal to, or plane the primitive will be co-planar with. depth {:func:`colour.geometry.primitive_grid`, :func:`colour.geometry.primitive_cube`}, Primitive depth. depth_segments {:func:`colour.geometry.primitive_grid`, :func:`colour.geometry.primitive_cube`}, Primitive 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, default 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, default 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`}, Primitive height. planes {:func:`colour.geometry.primitive_cube`}, Included grid primitives in the cube construction. width {:func:`colour.geometry.primitive_grid`, :func:`colour.geometry.primitive_cube`}, Primitive width. width_segments {:func:`colour.geometry.primitive_grid`, :func:`colour.geometry.primitive_cube`}, Primitive segments count along the width. height_segments {:func:`colour.geometry.primitive_grid`, :func:`colour.geometry.primitive_cube`}, Primitive segments count along the height. Returns ------- :class:`tuple` Tuple of primitive vertices, face indexes to produce a filled primitive and outline indexes to produce an outline of the faces 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))