Coverage for geometry/vertices.py: 74%

1"""

2 Geometry Primitive Vertices

3===========================

4

5 Define methods for generating vertices of fundamental geometric primitives

6 used in colour science visualisations and computations.

7

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

15

16 from __future__ import annotations

17

18 import typing

19

20 import numpy as np

21

22 from colour.algebra import spherical_to_cartesian

23 from colour.geometry import MAPPING_PLANE_TO_AXIS

24

25 if typing.TYPE_CHECKING:

26 from colour.hints import Any, ArrayLike, Literal, NDArrayFloat, Sequence

27

28 from 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)

39

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"

46

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]

55

56

57 def 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.

67

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.

81

82 Returns

83 -------

84 :class:`numpy.ndarray`

85 Quad primitive vertices.

86

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

95

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 )

100

101 u, v = tsplit(origin)

102

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 ]

124

125 return as_float_array(vertices)

126

127

128 def 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.

141

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.

161

162 Returns

163 -------

164 :class:`numpy.ndarray`

165 Grid primitive vertices.

166

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

190

191 u, v = tsplit(origin)

192

193 w_x, h_y = width / width_segments, height / height_segments

194

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 )

203

204 return as_float_array(quads)

205

206

207 def 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.

239

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.

261

262 Returns

263 -------

264 :class:`numpy.ndarray`

265 Cube primitive vertices.

266

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

300

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 )

306

307 u, v, w = tsplit(origin)

308

309 w_s, h_s, d_s = width_segments, height_segments, depth_segments

310

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 )

322

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 )

333

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 )

344

345 return as_float_array(grids)

346

347

348 def 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.

357

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.

377

378 Returns

379 -------

380 :class:`numpy.ndarray`

381 Sphere primitive vertices.

382

383 Notes

384 -----

385 - The sphere poles have latitude segments count - 1 co-located vertices.

386

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

414

415 origin = as_float_array(origin)

416

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 )

421

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 )

452

453 rho = ones(phi.shape) * radius

454 rho_theta_phi = tstack([rho, theta, phi])

455

456 vertices = spherical_to_cartesian(rho_theta_phi)

457

458 # Removing extra longitude vertices.

459 vertices = vertices[:-1, :, :]

460

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)

467

468 vertices += origin

469

470 return vertices

471

472

473 PRIMITIVE_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)

481 PRIMITIVE_VERTICES_METHODS.__doc__ = """

482 Supported methods for generating vertices of geometry primitives.

483"""

484

485

486 def 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.

492

493 Parameters

494 ----------

495 method

496 Method for generating primitive vertices.

497

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.

562

563 Returns

564 -------

565 :class:`numpy.ndarray`

566 Vertices of the primitive.

567

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

630

631 method = validate_method(method, tuple(PRIMITIVE_VERTICES_METHODS))

632

633 function = PRIMITIVE_VERTICES_METHODS[method]

634

635 return function(**filter_kwargs(function, **kwargs))