Coverage for geometry/vertices.py: 74%
78 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Geometry Primitive Vertices
3===========================
5Define methods for generating vertices of fundamental geometric primitives
6used in colour science visualisations and computations.
8- :func:`colour.geometry.primitive_vertices_quad_mpl`
9- :func:`colour.geometry.primitive_vertices_grid_mpl`
10- :func:`colour.geometry.primitive_vertices_cube_mpl`
11- :func:`colour.geometry.primitive_vertices_sphere`
12- :func:`colour.PRIMITIVE_VERTICES_METHODS`
13- :func:`colour.primitive_vertices`
14"""
16from __future__ import annotations
18import typing
20import numpy as np
22from colour.algebra import spherical_to_cartesian
23from colour.geometry import MAPPING_PLANE_TO_AXIS
25if typing.TYPE_CHECKING:
26 from colour.hints import Any, ArrayLike, Literal, NDArrayFloat, Sequence
28from colour.utilities import (
29 CanonicalMapping,
30 as_float_array,
31 filter_kwargs,
32 full,
33 ones,
34 tsplit,
35 tstack,
36 validate_method,
37 zeros,
38)
40__author__ = "Colour Developers"
41__copyright__ = "Copyright 2013 Colour Developers"
42__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
43__maintainer__ = "Colour Developers"
44__email__ = "colour-developers@colour-science.org"
45__status__ = "Production"
47__all__ = [
48 "primitive_vertices_quad_mpl",
49 "primitive_vertices_grid_mpl",
50 "primitive_vertices_cube_mpl",
51 "primitive_vertices_sphere",
52 "PRIMITIVE_VERTICES_METHODS",
53 "primitive_vertices",
54]
57def primitive_vertices_quad_mpl(
58 width: float = 1,
59 height: float = 1,
60 depth: float = 0,
61 origin: ArrayLike = (0, 0),
62 axis: Literal["+z", "+x", "+y", "yz", "xz", "xy"] | str = "+z",
63) -> NDArrayFloat:
64 """
65 Generate vertices of a quad primitive for use with *Matplotlib*
66 :class:`mpl_toolkits.mplot3d.art3d.Poly3DCollection` class.
68 Parameters
69 ----------
70 width
71 Width of the primitive.
72 height
73 Height of the primitive.
74 depth
75 Depth of the primitive.
76 origin
77 Origin of the primitive on the construction plane.
78 axis
79 Axis to which the primitive will be normal, or plane with which the
80 primitive will be co-planar.
82 Returns
83 -------
84 :class:`numpy.ndarray`
85 Quad primitive vertices.
87 Examples
88 --------
89 >>> primitive_vertices_quad_mpl()
90 array([[ 0., 0., 0.],
91 [ 1., 0., 0.],
92 [ 1., 1., 0.],
93 [ 0., 1., 0.]])
94 """
96 axis = MAPPING_PLANE_TO_AXIS.get(axis, axis).lower()
97 axis = validate_method(
98 axis, ("+x", "+y", "+z"), '"{0}" axis invalid, it must be one of {1}!'
99 )
101 u, v = tsplit(origin)
103 if axis == "+z":
104 vertices = [
105 (u, v, depth),
106 (u + width, v, depth),
107 (u + width, v + height, depth),
108 (u, v + height, depth),
109 ]
110 elif axis == "+y":
111 vertices = [
112 (u, depth, v),
113 (u + width, depth, v),
114 (u + width, depth, v + height),
115 (u, depth, v + height),
116 ]
117 elif axis == "+x":
118 vertices = [
119 (depth, u, v),
120 (depth, u + width, v),
121 (depth, u + width, v + height),
122 (depth, u, v + height),
123 ]
125 return as_float_array(vertices)
128def primitive_vertices_grid_mpl(
129 width: float = 1,
130 height: float = 1,
131 depth: float = 0,
132 width_segments: int = 1,
133 height_segments: int = 1,
134 origin: ArrayLike = (0, 0),
135 axis: Literal["+z", "+x", "+y", "yz", "xz", "xy"] | str = "+z",
136) -> NDArrayFloat:
137 """
138 Generate vertices for a grid primitive composed of quadrilateral
139 primitives for use with *Matplotlib*
140 :class:`mpl_toolkits.mplot3d.art3d.Poly3DCollection` class.
142 Parameters
143 ----------
144 width
145 Width of the primitive.
146 height
147 Height of the primitive.
148 depth
149 Depth of the primitive.
150 width_segments:
151 Number of width segments defining quad primitive counts along
152 the width axis.
153 height_segments:
154 Number of height segments defining quad primitive counts along
155 the height axis.
156 origin
157 Origin of the primitive on the construction plane.
158 axis
159 Axis to which the primitive will be normal, or plane with which the
160 primitive will be co-planar.
162 Returns
163 -------
164 :class:`numpy.ndarray`
165 Grid primitive vertices.
167 Examples
168 --------
169 >>> primitive_vertices_grid_mpl(width_segments=2, height_segments=2)
170 array([[[ 0. , 0. , 0. ],
171 [ 0.5, 0. , 0. ],
172 [ 0.5, 0.5, 0. ],
173 [ 0. , 0.5, 0. ]],
174 <BLANKLINE>
175 [[ 0. , 0.5, 0. ],
176 [ 0.5, 0.5, 0. ],
177 [ 0.5, 1. , 0. ],
178 [ 0. , 1. , 0. ]],
179 <BLANKLINE>
180 [[ 0.5, 0. , 0. ],
181 [ 1. , 0. , 0. ],
182 [ 1. , 0.5, 0. ],
183 [ 0.5, 0.5, 0. ]],
184 <BLANKLINE>
185 [[ 0.5, 0.5, 0. ],
186 [ 1. , 0.5, 0. ],
187 [ 1. , 1. , 0. ],
188 [ 0.5, 1. , 0. ]]])
189 """
191 u, v = tsplit(origin)
193 w_x, h_y = width / width_segments, height / height_segments
195 quads = []
196 for i in range(width_segments):
197 for j in range(height_segments):
198 quads.append( # noqa: PERF401
199 primitive_vertices_quad_mpl(
200 w_x, h_y, depth, (i * w_x + u, j * h_y + v), axis
201 )
202 )
204 return as_float_array(quads)
207def primitive_vertices_cube_mpl(
208 width: float = 1,
209 height: float = 1,
210 depth: float = 1,
211 width_segments: int = 1,
212 height_segments: int = 1,
213 depth_segments: int = 1,
214 origin: ArrayLike = (0, 0, 0),
215 planes: (
216 Sequence[
217 Literal[
218 "-x",
219 "+x",
220 "-y",
221 "+y",
222 "-z",
223 "+z",
224 "xy",
225 "xz",
226 "yz",
227 "yx",
228 "zx",
229 "zy",
230 ]
231 ]
232 | None
233 ) = None,
234) -> NDArrayFloat:
235 """
236 Generate vertices of a cube primitive made of grid primitives for
237 use with *Matplotlib* :class:`mpl_toolkits.mplot3d.art3d.Poly3DCollection`
238 class.
240 Parameters
241 ----------
242 width
243 Width of the primitive.
244 height
245 Height of the primitive.
246 depth
247 Depth of the primitive.
248 width_segments
249 Number of width segments defining quad primitive counts along
250 the width axis.
251 height_segments
252 Number of height segments defining quad primitive counts along
253 the height axis.
254 depth_segments
255 Number of depth segments defining quad primitive counts along
256 the depth axis.
257 origin
258 Origin of the primitive.
259 planes
260 Grid primitives to include in the cube construction.
262 Returns
263 -------
264 :class:`numpy.ndarray`
265 Cube primitive vertices.
267 Examples
268 --------
269 >>> primitive_vertices_cube_mpl()
270 array([[[ 0., 0., 0.],
271 [ 1., 0., 0.],
272 [ 1., 1., 0.],
273 [ 0., 1., 0.]],
274 <BLANKLINE>
275 [[ 0., 0., 1.],
276 [ 1., 0., 1.],
277 [ 1., 1., 1.],
278 [ 0., 1., 1.]],
279 <BLANKLINE>
280 [[ 0., 0., 0.],
281 [ 1., 0., 0.],
282 [ 1., 0., 1.],
283 [ 0., 0., 1.]],
284 <BLANKLINE>
285 [[ 0., 1., 0.],
286 [ 1., 1., 0.],
287 [ 1., 1., 1.],
288 [ 0., 1., 1.]],
289 <BLANKLINE>
290 [[ 0., 0., 0.],
291 [ 0., 1., 0.],
292 [ 0., 1., 1.],
293 [ 0., 0., 1.]],
294 <BLANKLINE>
295 [[ 1., 0., 0.],
296 [ 1., 1., 0.],
297 [ 1., 1., 1.],
298 [ 1., 0., 1.]]])
299 """
301 axis = (
302 sorted(MAPPING_PLANE_TO_AXIS.values())
303 if planes is None
304 else [MAPPING_PLANE_TO_AXIS.get(plane, plane).lower() for plane in planes]
305 )
307 u, v, w = tsplit(origin)
309 w_s, h_s, d_s = width_segments, height_segments, depth_segments
311 grids: list = []
312 if "-z" in axis:
313 grids.extend(
314 primitive_vertices_grid_mpl(width, depth, v, w_s, d_s, (u, w), "+z")
315 )
316 if "+z" in axis:
317 grids.extend(
318 primitive_vertices_grid_mpl(
319 width, depth, v + height, w_s, d_s, (u, w), "+z"
320 )
321 )
323 if "-y" in axis:
324 grids.extend(
325 primitive_vertices_grid_mpl(width, height, w, w_s, h_s, (u, v), "+y")
326 )
327 if "+y" in axis:
328 grids.extend(
329 primitive_vertices_grid_mpl(
330 width, height, w + depth, w_s, h_s, (u, v), "+y"
331 )
332 )
334 if "-x" in axis:
335 grids.extend(
336 primitive_vertices_grid_mpl(depth, height, u, d_s, h_s, (w, v), "+x")
337 )
338 if "+x" in axis:
339 grids.extend(
340 primitive_vertices_grid_mpl(
341 depth, height, u + width, d_s, h_s, (w, v), "+x"
342 )
343 )
345 return as_float_array(grids)
348def primitive_vertices_sphere(
349 radius: float = 0.5,
350 segments: int = 8,
351 intermediate: bool = False,
352 origin: ArrayLike = (0, 0, 0),
353 axis: Literal["+z", "+x", "+y", "yz", "xz", "xy"] | str = "+z",
354) -> NDArrayFloat:
355 """
356 Generate vertices of a latitude-longitude sphere primitive.
358 Parameters
359 ----------
360 radius
361 Radius of the sphere.
362 segments
363 Number of latitude-longitude segments. If the ``intermediate``
364 argument is *True*, the sphere will have one fewer segment
365 along its longitude.
366 intermediate
367 Whether to generate sphere vertices at the centres of the faces
368 outlined by the segments of a regular sphere generated without
369 the ``intermediate`` argument set to *True*. The resulting
370 sphere is inscribed within the regular sphere faces while
371 maintaining identical poles.
372 origin
373 Origin of the primitive on the construction plane.
374 axis
375 Axis to which the primitive will be normal, or plane with which the
376 primitive will be co-planar.
378 Returns
379 -------
380 :class:`numpy.ndarray`
381 Sphere primitive vertices.
383 Notes
384 -----
385 - The sphere poles have latitude segments count - 1 co-located vertices.
387 Examples
388 --------
389 >>> primitive_vertices_sphere(segments=4) # doctest: +ELLIPSIS
390 array([[[ 0.0000000...e+00, 0.0000000...e+00, 5.0000000...e-01],
391 [ -3.5355339...e-01, -4.3297802...e-17, 3.5355339...e-01],
392 [ -5.0000000...e-01, -6.1232340...e-17, 3.0616170...e-17],
393 [ -3.5355339...e-01, -4.3297802...e-17, -3.5355339...e-01],
394 [ -6.1232340...e-17, -7.4987989...e-33, -5.0000000...e-01]],
395 <BLANKLINE>
396 [[ 0.0000000...e+00, 0.0000000...e+00, 5.0000000...e-01],
397 [ 2.1648901...e-17, -3.5355339...e-01, 3.5355339...e-01],
398 [ 3.0616170...e-17, -5.0000000...e-01, 3.0616170...e-17],
399 [ 2.1648901...e-17, -3.5355339...e-01, -3.5355339...e-01],
400 [ 3.7493994...e-33, -6.1232340...e-17, -5.0000000...e-01]],
401 <BLANKLINE>
402 [[ 0.0000000...e+00, 0.0000000...e+00, 5.0000000...e-01],
403 [ 3.5355339...e-01, 0.0000000...e+00, 3.5355339...e-01],
404 [ 5.0000000...e-01, 0.0000000...e+00, 3.0616170...e-17],
405 [ 3.5355339...e-01, 0.0000000...e+00, -3.5355339...e-01],
406 [ 6.1232340...e-17, 0.0000000...e+00, -5.0000000...e-01]],
407 <BLANKLINE>
408 [[ 0.0000000...e+00, 0.0000000...e+00, 5.0000000...e-01],
409 [ 2.1648901...e-17, 3.5355339...e-01, 3.5355339...e-01],
410 [ 3.0616170...e-17, 5.0000000...e-01, 3.0616170...e-17],
411 [ 2.1648901...e-17, 3.5355339...e-01, -3.5355339...e-01],
412 [ 3.7493994...e-33, 6.1232340...e-17, -5.0000000...e-01]]])
413 """
415 origin = as_float_array(origin)
417 axis = MAPPING_PLANE_TO_AXIS.get(axis, axis).lower()
418 axis = validate_method(
419 axis, ("+x", "+y", "+z"), '"{0}" axis invalid, it must be one of {1}!'
420 )
422 if not intermediate:
423 theta = np.tile(
424 np.radians(np.linspace(0, 180, segments + 1)),
425 (int(segments) + 1, 1),
426 )
427 phi = np.transpose(
428 np.tile(
429 np.radians(np.linspace(-180, 180, segments + 1)),
430 (int(segments) + 1, 1),
431 )
432 )
433 else:
434 theta = np.tile(
435 np.radians(np.linspace(0, 180, segments * 2 + 1)[1::2][1:-1]),
436 (int(segments) + 1, 1),
437 )
438 theta = np.hstack(
439 [
440 zeros((segments + 1, 1)),
441 theta,
442 full((segments + 1, 1), np.pi),
443 ]
444 )
445 phi = np.transpose(
446 np.tile(
447 np.radians(np.linspace(-180, 180, segments + 1))
448 + np.radians(360 / segments / 2),
449 (int(segments), 1),
450 )
451 )
453 rho = ones(phi.shape) * radius
454 rho_theta_phi = tstack([rho, theta, phi])
456 vertices = spherical_to_cartesian(rho_theta_phi)
458 # Removing extra longitude vertices.
459 vertices = vertices[:-1, :, :]
461 if axis == "+z":
462 pass
463 elif axis == "+y":
464 vertices = np.roll(vertices, 2, -1)
465 elif axis == "+x":
466 vertices = np.roll(vertices, 1, -1)
468 vertices += origin
470 return vertices
473PRIMITIVE_VERTICES_METHODS: CanonicalMapping = CanonicalMapping(
474 {
475 "Quad MPL": primitive_vertices_quad_mpl,
476 "Grid MPL": primitive_vertices_grid_mpl,
477 "Cube MPL": primitive_vertices_cube_mpl,
478 "Sphere": primitive_vertices_sphere,
479 }
480)
481PRIMITIVE_VERTICES_METHODS.__doc__ = """
482Supported methods for generating vertices of geometry primitives.
483"""
486def primitive_vertices(
487 method: (Literal["Cube MPL", "Quad MPL", "Grid MPL", "Sphere"] | str) = "Cube MPL",
488 **kwargs: Any,
489) -> NDArrayFloat:
490 """
491 Generate vertices of a geometry primitive.
493 Parameters
494 ----------
495 method
496 Method for generating primitive vertices.
498 Other Parameters
499 ----------------
500 axis
501 {:func:`colour.geometry.primitive_vertices_quad_mpl`,
502 :func:`colour.geometry.primitive_vertices_grid_mpl`,
503 :func:`colour.geometry.primitive_vertices_sphere`},
504 **{'+z', '+x', '+y', 'yz', 'xz', 'xy'}**,
505 Axis to which the primitive will be normal, or plane with which
506 the primitive will be co-planar.
507 depth
508 {:func:`colour.geometry.primitive_vertices_quad_mpl`,
509 :func:`colour.geometry.primitive_vertices_grid_mpl`,
510 :func:`colour.geometry.primitive_vertices_cube_mpl`},
511 Depth of the primitive.
512 depth_segments
513 {:func:`colour.geometry.primitive_vertices_cube_mpl`},
514 Number of depth segments defining quad primitive counts along
515 the depth axis.
516 height
517 {:func:`colour.geometry.primitive_vertices_quad_mpl`,
518 :func:`colour.geometry.primitive_vertices_grid_mpl`,
519 :func:`colour.geometry.primitive_vertices_cube_mpl`},
520 Height of the primitive.
521 height_segments
522 {:func:`colour.geometry.primitive_vertices_grid_mpl`,
523 :func:`colour.geometry.primitive_vertices_cube_mpl`},
524 Number of height segments defining quad primitive counts along
525 the height axis.
526 intermediate
527 {:func:`colour.geometry.primitive_vertices_sphere`},
528 Whether to generate sphere vertices at the centres of the faces
529 outlined by the segments of a regular sphere generated without
530 the ``intermediate`` argument set to *True*. The resulting
531 sphere is inscribed within the regular sphere faces while
532 maintaining identical poles.
533 origin
534 {:func:`colour.geometry.primitive_vertices_quad_mpl`,
535 :func:`colour.geometry.primitive_vertices_grid_mpl`,
536 :func:`colour.geometry.primitive_vertices_cube_mpl`,
537 :func:`colour.geometry.primitive_vertices_sphere`},
538 Origin of the primitive on the construction plane.
539 planes
540 {:func:`colour.geometry.primitive_vertices_cube_mpl`},
541 **{'-x', '+x', '-y', '+y', '-z', '+z',
542 'xy', 'xz', 'yz', 'yx', 'zx', 'zy'}**,
543 Grid primitives to include in the cube construction.
544 radius
545 {:func:`colour.geometry.primitive_vertices_sphere`},
546 Radius of the sphere.
547 segments
548 {:func:`colour.geometry.primitive_vertices_sphere`},
549 Number of latitude-longitude segments. If the ``intermediate``
550 argument is *True*, the sphere will have one fewer segment
551 along its longitude.
552 width
553 {:func:`colour.geometry.primitive_vertices_quad_mpl`,
554 :func:`colour.geometry.primitive_vertices_grid_mpl`,
555 :func:`colour.geometry.primitive_vertices_cube_mpl`},
556 Width of the primitive.
557 width_segments
558 {:func:`colour.geometry.primitive_vertices_grid_mpl`,
559 :func:`colour.geometry.primitive_vertices_cube_mpl`},
560 Number of width segments defining quad primitive counts along
561 the width axis.
563 Returns
564 -------
565 :class:`numpy.ndarray`
566 Vertices of the primitive.
568 Examples
569 --------
570 >>> primitive_vertices()
571 array([[[ 0., 0., 0.],
572 [ 1., 0., 0.],
573 [ 1., 1., 0.],
574 [ 0., 1., 0.]],
575 <BLANKLINE>
576 [[ 0., 0., 1.],
577 [ 1., 0., 1.],
578 [ 1., 1., 1.],
579 [ 0., 1., 1.]],
580 <BLANKLINE>
581 [[ 0., 0., 0.],
582 [ 1., 0., 0.],
583 [ 1., 0., 1.],
584 [ 0., 0., 1.]],
585 <BLANKLINE>
586 [[ 0., 1., 0.],
587 [ 1., 1., 0.],
588 [ 1., 1., 1.],
589 [ 0., 1., 1.]],
590 <BLANKLINE>
591 [[ 0., 0., 0.],
592 [ 0., 1., 0.],
593 [ 0., 1., 1.],
594 [ 0., 0., 1.]],
595 <BLANKLINE>
596 [[ 1., 0., 0.],
597 [ 1., 1., 0.],
598 [ 1., 1., 1.],
599 [ 1., 0., 1.]]])
600 >>> primitive_vertices("Quad MPL")
601 array([[ 0., 0., 0.],
602 [ 1., 0., 0.],
603 [ 1., 1., 0.],
604 [ 0., 1., 0.]])
605 >>> primitive_vertices("Sphere", segments=4) # doctest: +ELLIPSIS
606 array([[[ 0.0000000...e+00, 0.0000000...e+00, 5.0000000...e-01],
607 [ -3.5355339...e-01, -4.3297802...e-17, 3.5355339...e-01],
608 [ -5.0000000...e-01, -6.1232340...e-17, 3.0616170...e-17],
609 [ -3.5355339...e-01, -4.3297802...e-17, -3.5355339...e-01],
610 [ -6.1232340...e-17, -7.4987989...e-33, -5.0000000...e-01]],
611 <BLANKLINE>
612 [[ 0.0000000...e+00, 0.0000000...e+00, 5.0000000...e-01],
613 [ 2.1648901...e-17, -3.5355339...e-01, 3.5355339...e-01],
614 [ 3.0616170...e-17, -5.0000000...e-01, 3.0616170...e-17],
615 [ 2.1648901...e-17, -3.5355339...e-01, -3.5355339...e-01],
616 [ 3.7493994...e-33, -6.1232340...e-17, -5.0000000...e-01]],
617 <BLANKLINE>
618 [[ 0.0000000...e+00, 0.0000000...e+00, 5.0000000...e-01],
619 [ 3.5355339...e-01, 0.0000000...e+00, 3.5355339...e-01],
620 [ 5.0000000...e-01, 0.0000000...e+00, 3.0616170...e-17],
621 [ 3.5355339...e-01, 0.0000000...e+00, -3.5355339...e-01],
622 [ 6.1232340...e-17, 0.0000000...e+00, -5.0000000...e-01]],
623 <BLANKLINE>
624 [[ 0.0000000...e+00, 0.0000000...e+00, 5.0000000...e-01],
625 [ 2.1648901...e-17, 3.5355339...e-01, 3.5355339...e-01],
626 [ 3.0616170...e-17, 5.0000000...e-01, 3.0616170...e-17],
627 [ 2.1648901...e-17, 3.5355339...e-01, -3.5355339...e-01],
628 [ 3.7493994...e-33, 6.1232340...e-17, -5.0000000...e-01]]])
629 """
631 method = validate_method(method, tuple(PRIMITIVE_VERTICES_METHODS))
633 function = PRIMITIVE_VERTICES_METHODS[method]
635 return function(**filter_kwargs(function, **kwargs))