from itertools import product
from typing import List as TypingList
from typing import Union
import numpy as np
import k3d
def _wind_outwards(vertices, indices):
"""Return `indices` with every triangle wound counter-clockwise seen from outside.
These solids are convex and centred on their own centroid, so the test is the sign of the
face normal against the direction from the centroid to the face.
"""
v = np.asarray(vertices, np.float64).reshape(-1, 3)
tris = np.asarray(indices, np.int64).reshape(-1, 3)
centre = v.mean(axis=0)
out = []
for a, b, c in tris:
normal = np.cross(v[b] - v[a], v[c] - v[a])
if np.dot(normal, (v[a] + v[b] + v[c]) / 3.0 - centre) < 0.0:
a, b, c = a, c, b
out += [int(a), int(b), int(c)]
return out
class PlatonicSolid:
"""Base class for platonic solids."""
# quoted: k3d/__init__ imports this module, so k3d.objects is not bound yet when the
# class body runs. Sphinx and typing.get_type_hints resolve them later, once it is.
@property
def mesh(self) -> "k3d.objects.geometry.Mesh":
"""Return k3d.mesh object of the solid."""
return k3d.mesh(self.vertices, self.indices, side="double")
@property
def points(self) -> "k3d.objects.points.Points":
"""Return k3d.points object of solid vertices."""
return k3d.points(
self.vertices,
point_size=np.sqrt(np.sum((self.vertices[0] - self.vertices[1]) ** 2)) / 20,
)
[docs]
class Dodecahedron(PlatonicSolid):
"""Create a dodecahedron solid."""
def __init__(
self, origin: Union[TypingList[float], np.ndarray] = [0, 0, 0], size: float = 1
) -> None:
"""Inits Dodecahedron with an origin and a size.
Parameters
----------
origin : list, optional
The position of centroid of the solid, by default [0, 0, 0].
size : int, optional
The size*sqrt(3) is the distance of each vertex of the solid, by default 1.
Raises
------
TypeError
Origin should have 3 coordinates.
"""
origin = np.array(origin, dtype=np.float32)
if origin.shape == (3,):
fi = (1 + np.sqrt(5)) / 2
dodecahedron_vertices = list(product([-1, 1], [-1, 1], [-1, 1]))
dodecahedron_vertices += [
(0, fi, 1 / fi),
(0, -fi, 1 / fi),
(0, -fi, -1 / fi),
(0, fi, -1 / fi),
(1 / fi, 0, fi),
(-1 / fi, 0, fi),
(-1 / fi, 0, -fi),
(1 / fi, 0, -fi),
(fi, 1 / fi, 0),
(-fi, 1 / fi, 0),
(-fi, -1 / fi, 0),
(fi, -1 / fi, 0),
]
dodecahedron_vertices = np.float32(
size * np.array(dodecahedron_vertices) + origin
)
self.vertices = dodecahedron_vertices
self.indices = [
0,
1,
18,
0,
1,
10,
1,
9,
10,
0,
10,
14,
10,
14,
15,
4,
10,
15,
4,
9,
10,
4,
5,
9,
4,
5,
19,
4,
15,
19,
6,
15,
19,
6,
16,
19,
6,
7,
16,
6,
7,
8,
6,
8,
11,
2,
3,
17,
2,
3,
8,
2,
8,
11,
1,
3,
13,
1,
3,
18,
3,
17,
18,
1,
9,
13,
9,
12,
13,
5,
9,
12,
5,
12,
19,
12,
16,
19,
7,
12,
16,
3,
7,
8,
3,
7,
12,
3,
12,
13,
14,
6,
15,
14,
6,
11,
2,
11,
14,
0,
17,
18,
0,
2,
17,
0,
2,
14,
]
self.indices = _wind_outwards(self.vertices, self.indices)
else:
raise TypeError("Origin should have 3 coordinates.")
[docs]
class Cube(PlatonicSolid):
"""Create a cube."""
def __init__(
self, origin: Union[TypingList[float], np.ndarray] = [0, 0, 0], size: float = 1
) -> None:
"""Inits Cube with an origin and a size.
Parameters
----------
origin : list, optional
The position of centroid of the solid, by default [0, 0, 0].
size : int, optional
The size*sqrt(3) is the distance of each vertex from centroid of the solid, by default 1.
Raises
------
TypeError
Origin attribute should have 3 coordinates.
"""
origin = np.array(origin, dtype=np.float32)
if origin.shape == (3,):
cube_vertices = np.array(
list(product([1, -1], [1, -1], [1, -1])), np.float32
)
cube_vertices = np.float32(size * cube_vertices + origin)
self.vertices = cube_vertices
self.indices = [
0,
1,
2,
1,
2,
3,
0,
1,
4,
1,
4,
5,
1,
3,
5,
3,
5,
7,
0,
2,
4,
2,
4,
6,
2,
3,
7,
2,
6,
7,
4,
5,
6,
5,
6,
7,
]
self.indices = _wind_outwards(self.vertices, self.indices)
else:
raise TypeError("Origin attribute should have 3 coordinates.")
[docs]
class Icosahedron(PlatonicSolid):
"""Create a icosahedron solid."""
def __init__(
self, origin: Union[TypingList[float], np.ndarray] = [0, 0, 0], size: float = 1
) -> None:
"""Inits Icosahedron with an origne and a size.
Parameters
----------
origin : list, optional
The position of centroid of the solid, by default [0, 0, 0].
size : int, optional
The size of the solid, by default 1.
Raises
------
TypeError
Origin attribute should have 3 coordinates.
"""
origin = np.array(origin, dtype=np.float32)
if origin.shape == (3,):
fi = (1 + np.sqrt(5)) / 2
icosahedron_vertices = [
(1, fi, 0),
(1, -fi, 0),
(-1, fi, 0),
(-1, -fi, 0),
(fi, 0, 1),
(fi, 0, -1),
(-fi, 0, 1),
(-fi, 0, -1),
(0, 1, fi),
(0, 1, -fi),
(0, -1, fi),
(0, -1, -fi),
]
icosahedron_vertices = np.float32(
size * np.array(icosahedron_vertices) + origin
)
self.vertices = icosahedron_vertices
self.indices = [
0,
2,
8,
0,
4,
8,
0,
2,
9,
0,
5,
9,
2,
6,
8,
2,
7,
9,
2,
6,
7,
0,
4,
5,
1,
4,
5,
1,
5,
11,
7,
9,
11,
3,
7,
11,
3,
6,
7,
3,
6,
10,
4,
8,
10,
6,
8,
10,
1,
4,
10,
1,
3,
11,
1,
3,
10,
5,
9,
11,
]
self.indices = _wind_outwards(self.vertices, self.indices)
else:
raise TypeError("Origin attribute should have 3 coordinates.")
[docs]
class Octahedron(PlatonicSolid):
"""Create a octahedron solid."""
def __init__(
self, origin: Union[TypingList[float], np.ndarray] = [0, 0, 0], size: float = 1
) -> None:
"""Inits Octahedron with an origin and a size
Parameters
----------
origin : list, optional
The position of centroid of the solid, by default [0, 0, 0].
size : int, optional
The distance of each vertex from centroid of the solid, by default 1.
Raises
------
TypeError
Origin attribute should have 3 coordinates.
"""
origin = np.array(origin, dtype=np.float32)
if origin.shape == (3,):
octahedron_vertices = [
(1, 0, 0),
(0, 1, 0),
(0, 0, 1),
(-1, 0, 0),
(0, -1, 0),
(0, 0, -1),
]
octahedron_vertices = np.float32(
size * np.array(octahedron_vertices) + origin
)
self.vertices = octahedron_vertices
self.indices = [
0,
1,
2,
0,
1,
5,
1,
2,
3,
1,
3,
5,
0,
4,
5,
0,
2,
4,
2,
3,
4,
3,
4,
5,
]
self.indices = _wind_outwards(self.vertices, self.indices)
else:
raise TypeError("Origin attribute should have 3 coordinates.")
[docs]
class Tetrahedron(PlatonicSolid):
"""Create a tetrahedron solid."""
def __init__(
self, origin: Union[TypingList[float], np.ndarray] = [0, 0, 0], size: float = 1
) -> None:
"""Inits Tetrahedron with an origin and a size.
Parameters
----------
origin : list, optional
The position of centroid of the solid, by default [0, 0, 0].
size : int, optional
The size*sqrt(3) is the distance of each vertex from centroid of the solid, by default 1.
Raises
------
TypeError
Origin attribute should have 3 coordinates.
"""
origin = np.array(origin, dtype=np.float32)
if origin.shape == (3,):
tetrahedron_vertices = [(1, 1, 1), (1, -1, -1), (-1, 1, -1), (-1, -1, 1)]
tetrahedron_vertices = np.float32(
size * np.array(tetrahedron_vertices) + origin
)
self.vertices = tetrahedron_vertices
self.indices = [0, 1, 2, 0, 1, 3, 1, 2, 3, 0, 2, 3]
self.indices = _wind_outwards(self.vertices, self.indices)
else:
raise TypeError("Origin attribute should have 3 coordinates.")