"""
Geometry / Hull Section
=======================
Define 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 np.reshape(as_float_array(edges_ordered), (-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