Source code for colour.geometry.section

"""
Geometry / Hull Section
=======================

Defines various objects to compute hull sections:

-   :func:`colour.geometry.hull_section`
"""

from __future__ import annotations

import numpy as np

from colour.algebra import linear_conversion
from colour.constants import DTYPE_FLOAT_DEFAULT
from colour.hints import Any, ArrayLike, Literal, NDArrayFloat, cast
from colour.utilities import (
    as_float_array,
    as_float_scalar,
    required,
    validate_method,
)

__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__ = [
    "edges_to_chord",
    "unique_vertices",
    "close_chord",
    "hull_section",
]


def edges_to_chord(edges: ArrayLike, index: int = 0) -> NDArrayFloat:
    """
    Convert given edges to a chord, starting at given index.

    Parameters
    ----------
    edges
        Edges to convert to a chord.
    index
        Index to start forming the chord at.

    Returns
    -------
    :class:`numpy.ndarray`
        Chord.

    Examples
    --------
    >>> edges = np.array(
    ...     [
    ...         [[-0.0, -0.5, 0.0], [0.5, -0.5, 0.0]],
    ...         [[-0.5, -0.5, 0.0], [-0.0, -0.5, 0.0]],
    ...         [[0.5, 0.5, 0.0], [-0.0, 0.5, 0.0]],
    ...         [[-0.0, 0.5, 0.0], [-0.5, 0.5, 0.0]],
    ...         [[-0.5, 0.0, -0.0], [-0.5, -0.5, -0.0]],
    ...         [[-0.5, 0.5, -0.0], [-0.5, 0.0, -0.0]],
    ...         [[0.5, -0.5, -0.0], [0.5, 0.0, -0.0]],
    ...         [[0.5, 0.0, -0.0], [0.5, 0.5, -0.0]],
    ...     ]
    ... )
    >>> edges_to_chord(edges)
    array([[-0. , -0.5,  0. ],
           [ 0.5, -0.5,  0. ],
           [ 0.5, -0.5, -0. ],
           [ 0.5,  0. , -0. ],
           [ 0.5,  0. , -0. ],
           [ 0.5,  0.5, -0. ],
           [ 0.5,  0.5,  0. ],
           [-0. ,  0.5,  0. ],
           [-0. ,  0.5,  0. ],
           [-0.5,  0.5,  0. ],
           [-0.5,  0.5, -0. ],
           [-0.5,  0. , -0. ],
           [-0.5,  0. , -0. ],
           [-0.5, -0.5, -0. ],
           [-0.5, -0.5,  0. ],
           [-0. , -0.5,  0. ]])
    """

    edge_list = as_float_array(edges).tolist()

    edges_ordered = [edge_list.pop(index)]
    segment = np.array(edges_ordered[0][1])

    while len(edge_list) > 0:
        edges_array = np.array(edge_list)
        d_0 = np.linalg.norm(edges_array[:, 0, :] - segment, axis=1)
        d_1 = np.linalg.norm(edges_array[:, 1, :] - segment, axis=1)
        d_0_argmin, d_1_argmin = d_0.argmin(), d_1.argmin()

        if d_0[d_0_argmin] < d_1[d_1_argmin]:
            edges_ordered.append(edge_list.pop(d_0_argmin))
            segment = np.array(edges_ordered[-1][1])
        else:
            edges_ordered.append(edge_list.pop(d_1_argmin))
            segment = np.array(edges_ordered[-1][0])

    return as_float_array(edges_ordered).reshape([-1, segment.shape[-1]])


def close_chord(vertices: ArrayLike) -> NDArrayFloat:
    """
    Close the chord.

    Parameters
    ----------
    vertices
        Vertices of the chord to close.

    Returns
    -------
    :class:`numpy.ndarray`
        Closed chord.

    Examples
    --------
    >>> close_chord(np.array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5]]))
    array([[ 0. ,  0.5,  0. ],
           [ 0. ,  0. ,  0.5],
           [ 0. ,  0.5,  0. ]])
    """

    vertices = as_float_array(vertices)

    return np.vstack([vertices, vertices[0]])


def unique_vertices(
    vertices: ArrayLike,
    decimals: int = np.finfo(cast(Any, DTYPE_FLOAT_DEFAULT)).precision - 1,
) -> NDArrayFloat:
    """
    Return the unique vertices from given vertices.

    Parameters
    ----------
    vertices
        Vertices to return the unique vertices from.
    decimals
        Decimals used when rounding the vertices prior to comparison.

    Returns
    -------
    :class:`numpy.ndarray`
        Unique vertices.

    Notes
    -----
    -   The vertices are rounded at given ``decimals``.

    Examples
    --------
    >>> unique_vertices(
    ...     np.array([[0.0, 0.5, 0.0], [0.0, 0.0, 0.5], [0.0, 0.5, 0.0]])
    ... )
    array([[ 0. ,  0.5,  0. ],
           [ 0. ,  0. ,  0.5]])
    """

    vertices = as_float_array(vertices)

    unique, indexes = np.unique(
        vertices.round(decimals=decimals), axis=0, return_index=True
    )

    return unique[np.argsort(indexes)]


[docs] @required("trimesh") def hull_section( hull: trimesh.Trimesh, # pyright: ignore # noqa: F821 axis: Literal["+z", "+x", "+y"] | str = "+z", origin: float = 0.5, normalise: bool = False, ) -> NDArrayFloat: """ Compute the hull section for given axis at given origin. Parameters ---------- hull *Trimesh* hull. axis Axis the hull section will be normal to. origin Coordinate along ``axis`` at which to plot the hull section. normalise Whether to normalise ``axis`` to the extent of the hull along it. Returns ------- :class:`numpy.ndarray` Hull section vertices. Examples -------- >>> from colour.geometry import primitive_cube >>> from colour.utilities import is_trimesh_installed >>> vertices, faces, outline = primitive_cube(1, 1, 1, 2, 2, 2) >>> if is_trimesh_installed: ... import trimesh ... ... hull = trimesh.Trimesh(vertices["position"], faces, process=False) ... hull_section(hull, origin=0) ... array([[-0. , -0.5, 0. ], [ 0.5, -0.5, 0. ], [ 0.5, 0. , -0. ], [ 0.5, 0.5, -0. ], [-0. , 0.5, 0. ], [-0.5, 0.5, 0. ], [-0.5, 0. , -0. ], [-0.5, -0.5, -0. ], [-0. , -0.5, 0. ]]) """ import trimesh.intersections axis = validate_method( axis, ("+z", "+x", "+y"), '"{0}" axis is invalid, it must be one of {1}!', ) if axis == "+x": normal, plane = np.array([1, 0, 0]), np.array([origin, 0, 0]) elif axis == "+y": normal, plane = np.array([0, 1, 0]), np.array([0, origin, 0]) elif axis == "+z": normal, plane = np.array([0, 0, 1]), np.array([0, 0, origin]) if normalise: vertices = hull.vertices * normal origin = as_float_scalar( linear_conversion( origin, [0, 1], [np.min(vertices), np.max(vertices)] ) ) plane[plane != 0] = origin section = trimesh.intersections.mesh_plane(hull, normal, plane) if len(section) == 0: raise ValueError( f'No section exists on "{axis}" axis at {origin} origin!' ) section = close_chord(unique_vertices(edges_to_chord(section))) return section