TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
133 / 145
91.7%
Branches:
98 / 113
86.7%
Functions:
22 / 24
91.7%

python/src/mesh.cpp
Line Branch Exec Source
1 #include "bindings.h"
2 #include "common.h"
3 #include "tables.h"
4
5 #include "terraingen/mesh/io/icq.h"
6 #include "terraingen/mesh/io/mesh_io.h"
7 #include "terraingen/mesh/mesh.h"
8 #include "terraingen/mesh/polyhedra.h"
9 #include "terraingen/mesh/shape_model.h"
10
11 #include <nanobind/ndarray.h>
12 #include <nanobind/operators.h>
13 #include <nanobind/stl/array.h>
14
15 #include <array>
16 #include <stdexcept>
17 #include <string>
18 #include <utility>
19 #include <vector>
20
21 namespace terraingen::python {
22
23 namespace {
24
25 using Points = nb::ndarray<const double, nb::ndim<2>, nb::device::cpu, nb::c_contig>;
26 using Indices = nb::ndarray<const std::uint32_t, nb::ndim<2>, nb::device::cpu, nb::c_contig>;
27
28 1 std::vector<Vec3d> ToVectors(const Points& array) {
29
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1 if (array.shape(1) != 3) {
30
0/1
✗ Branch 2 not taken.
✗ throw std::invalid_argument("Expected an array of shape (n, 3)");
31 }
32
1/1
✓ Branch 2 taken 1 time.
1 std::vector<Vec3d> points(array.shape(0));
33
2/2
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 1 time.
9 for (std::size_t i = 0; i < points.size(); ++i) {
34 8 points[i] = {array.data()[3 * i], array.data()[3 * i + 1], array.data()[3 * i + 2]};
35 }
36 1 return points;
37 }
38
39 27 nb::object VectorArray(const std::vector<Vec3d>& vectors) {
40 27 std::vector<double> flat;
41
1/1
✓ Branch 2 taken 27 times.
27 flat.reserve(3 * vectors.size());
42
2/2
✓ Branch 4 taken 32808 times.
✓ Branch 5 taken 27 times.
32835 for (const Vec3d& v : vectors) {
43
1/1
✓ Branch 3 taken 32808 times.
32808 flat.insert(flat.end(), {v.x, v.y, v.z});
44 }
45
3/3
✓ Branch 3 taken 27 times.
✓ Branch 6 taken 27 times.
✓ Branch 9 taken 27 times.
108 return Column(std::move(flat)).attr("reshape")(-1, 3);
46 27 }
47
48 25 nb::tuple MeshArrays(const TriangleMesh& mesh) {
49 25 std::vector<std::uint32_t> triangles;
50
1/1
✓ Branch 2 taken 25 times.
25 triangles.reserve(3 * mesh.triangles.size());
51
2/2
✓ Branch 4 taken 62240 times.
✓ Branch 5 taken 25 times.
62265 for (const auto& t : mesh.triangles) {
52
1/1
✓ Branch 5 taken 62240 times.
62240 triangles.insert(triangles.end(), t.begin(), t.end());
53 }
54
1/1
✓ Branch 1 taken 25 times.
50 return nb::make_tuple(VectorArray(mesh.positions),
55
4/4
✓ Branch 3 taken 25 times.
✓ Branch 6 taken 25 times.
✓ Branch 9 taken 25 times.
✓ Branch 12 taken 25 times.
150 Column(std::move(triangles)).attr("reshape")(-1, 3));
56 25 }
57
58 1 TriangleMesh MeshFromArrays(const Points& positions, const Indices& triangles) {
59
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1 if (triangles.shape(1) != 3) {
60
0/1
✗ Branch 2 not taken.
✗ throw std::invalid_argument("Triangles must have shape (m, 3)");
61 }
62 1 TriangleMesh mesh;
63
1/1
✓ Branch 1 taken 1 time.
1 mesh.positions = ToVectors(positions);
64
2/2
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 1 time.
13 for (std::size_t i = 0; i < triangles.shape(0); ++i) {
65 12 mesh.triangles.push_back(
66
1/1
✓ Branch 4 taken 12 times.
12 {triangles.data()[3 * i], triangles.data()[3 * i + 1], triangles.data()[3 * i + 2]});
67 }
68 1 return mesh;
69 }
70
71 20 std::vector<Vec3d> OptionalNormals(const std::optional<Points>& normals) {
72
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
20 return normals ? ToVectors(*normals) : std::vector<Vec3d>{};
73 }
74
75 44 void BindRigidTransform(nb::module_& m) {
76 ✗ nb::class_<RigidTransform>(m, "RigidTransform", "A rotation then a translation.")
77 44 .def(
78 "__init__",
79 ✗ [](RigidTransform* self, std::array<double, 9> rotation, Vec3d translation) {
80 ✗ new (self) RigidTransform{rotation, translation};
81 ✗ },
82
1/1
✓ Branch 1 taken 44 times.
44 "rotation"_a = std::array<double, 9>{1, 0, 0, 0, 1, 0, 0, 0, 1},
83
1/1
✓ Branch 1 taken 44 times.
88 "translation"_a = Vec3d{}, "`rotation` is a row-major 3 x 3 matrix.")
84
1/1
✓ Branch 1 taken 44 times.
44 .def_static("rotation_x", &RigidTransform::RotationX, "angle"_a, "Radians.")
85
1/1
✓ Branch 1 taken 44 times.
44 .def_static("rotation_y", &RigidTransform::RotationY, "angle"_a, "Radians.")
86
1/1
✓ Branch 1 taken 44 times.
44 .def_static("rotation_z", &RigidTransform::RotationZ, "angle"_a, "Radians.")
87
1/1
✓ Branch 1 taken 44 times.
88 .def_static("translation_by", &RigidTransform::Translation, "offset"_a)
88 44 .def_rw("rotation", &RigidTransform::rotation)
89 44 .def_rw("translation", &RigidTransform::translation)
90
1/1
✓ Branch 1 taken 44 times.
44 .def("apply", &RigidTransform::Apply, "point"_a)
91
1/1
✓ Branch 1 taken 44 times.
88 .def("after", &RigidTransform::After, "first"_a,
92 "The transform that applies `first`, then this one.");
93 44 }
94
95 44 void BindMeshFiles(nb::class_<TriangleMesh>& mesh) {
96 mesh.def(
97 "write",
98 64 [](const TriangleMesh& self, const std::filesystem::path& path,
99 const std::optional<Points>& normals, const MeshMaterial& material) {
100 20 WriteMesh(path, {self, OptionalNormals(normals), material});
101
4/8
✓ Branch 1 taken 20 times.
✓ Branch 4 taken 20 times.
✓ Branch 7 taken 20 times.
✓ Branch 10 taken 20 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
40 },
102
4/6
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
220 "path"_a, "normals"_a = nb::none(), "material"_a = MeshMaterial{},
103 "Writes the mesh as .obj (with an .mtl), .gltf (with a .bin), .glb or .stl, by extension.");
104 44 }
105
106 44 void BindTriangleMesh(nb::module_& m) {
107 44 nb::class_<TriangleMesh> mesh(m, "TriangleMesh", "A triangle mesh in metres.");
108 44 mesh.def(
109 "__init__",
110 ✗ [](TriangleMesh* self, const Points& positions, const Indices& triangles) {
111
1/3
✓ Branch 2 taken 1 time.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
1 new (self) TriangleMesh(MeshFromArrays(positions, triangles));
112 1 },
113
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 "positions"_a, "triangles"_a,
114 "From float64 positions of shape (n, 3) and uint32 triangles of shape (m, 3).")
115 88 .def_prop_ro("arrays", &MeshArrays,
116 "(positions, triangles) as NumPy arrays of shape (n, 3).")
117 57 .def_prop_ro("vertex_count", [](const TriangleMesh& self) { return self.positions.size(); })
118 44 .def_prop_ro("triangle_count",
119 30 [](const TriangleMesh& self) { return self.triangles.size(); })
120 44 .def_prop_ro(
121 "bounds",
122 ✗ [](const TriangleMesh& self) {
123
1/1
✓ Branch 1 taken 2 times.
2 const Box3d box = Bounds(self);
124 4 return std::pair{box.min, box.max};
125 },
126 "((min x, y, z), (max x, y, z)).")
127 44 .def_prop_ro("volume", &Volume, "The enclosed volume of a closed mesh, in cubic metres.")
128 88 .def_prop_ro("surface_area", &SurfaceArea, "The area of its triangles, in square metres.")
129 44 .def(
130 "vertex_normals",
131
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
1 [](const TriangleMesh& self) { return VectorArray(VertexNormals(self)); },
132 "Area-weighted unit normals, one per vertex, as an array of shape (n, 3).")
133 44 .def(
134 "with_creases",
135 ✗ [](const TriangleMesh& self, double creaseAngle) {
136
1/1
✓ Branch 1 taken 1 time.
1 const ShadedMesh shaded = WithCreases(self, creaseAngle);
137
2/2
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 1 time.
2 return nb::make_tuple(shaded.mesh, VectorArray(shaded.normals));
138 1 },
139
1/1
✓ Branch 1 taken 44 times.
44 "crease_angle"_a,
140 "(mesh, normals): vertices split along creases sharper than `crease_angle` radians, "
141 "with smooth normals elsewhere.")
142
1/1
✓ Branch 1 taken 44 times.
44 .def("transformed", &Transformed, "transform"_a)
143
1/1
✓ Branch 1 taken 44 times.
44 .def("scaled", &Scaled, "scale"_a, "The mesh scaled per axis.")
144
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 .def("subdivided", &Subdivide, "levels"_a, "on_unit_sphere"_a = false,
145 "Each triangle split into four `levels` times, new vertices on the unit sphere if "
146 "`on_unit_sphere`.");
147
1/1
✓ Branch 1 taken 44 times.
44 BindMeshFiles(mesh);
148 88 nb::enum_<BaseSolid>(m, "BaseSolid", "A solid boulder shapes start from.")
149 44 .value("ICOSAHEDRON", BaseSolid::Icosahedron)
150 44 .value("CUBE", BaseSolid::Cube)
151 44 .value("PYRAMID", BaseSolid::Pyramid)
152 44 .value("PRISM", BaseSolid::Prism)
153 .value("TETRAHEDRON", BaseSolid::Tetrahedron);
154
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("base_solid_mesh", &BaseSolidMesh, "solid"_a, "The mesh of a base solid.");
155
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("icosphere", &Icosphere, "levels"_a,
156 "A unit sphere: an icosahedron subdivided `levels` times.");
157 44 }
158
159 44 void BindScene(nb::module_& m) {
160 44 nb::class_<MeshMaterial>(m, "MeshMaterial", "A mesh's material: a name and a diffuse colour.")
161 44 .def(
162 "__init__",
163 ✗ [](MeshMaterial* self, std::string name, std::array<double, 3> colour) {
164 4 new (self) MeshMaterial{std::move(name), colour};
165 4 },
166
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
132 "name"_a = "default", "colour"_a = std::array<double, 3>{0.5, 0.5, 0.5},
167 "`colour` is linear RGB, each in [0, 1].")
168 44 .def_rw("name", &MeshMaterial::name)
169 44 .def_rw("colour", &MeshMaterial::colour)
170
1/1
✓ Branch 2 taken 44 times.
44 .def(nb::self == nb::self);
171 ✗ nb::class_<SceneMesh>(m, "SceneMesh", "A mesh and its material.")
172 44 .def(
173 "__init__",
174 47 [](SceneMesh* self, TriangleMesh mesh, MeshMaterial material) {
175 3 new (self) SceneMesh{std::move(mesh), {}, std::move(material)};
176 3 },
177
3/5
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
220 "mesh"_a, "material"_a = MeshMaterial{})
178 44 .def_rw("mesh", &SceneMesh::mesh)
179 .def_rw("material", &SceneMesh::material);
180
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("read_mesh", &ReadMesh, "path"_a,
181 "Every triangle of an .obj, .gltf, .glb, .stl or (kilometre) .icq file in z-up metres, "
182 "with its first "
183 "material.");
184
1/1
✓ Branch 1 taken 44 times.
44 m.def(
185 6 "is_mesh_path", [](const std::filesystem::path& path) { return IsMeshPath(path); },
186
1/1
✓ Branch 1 taken 44 times.
88 "path"_a, "Whether the extension names a mesh format: .obj, .gltf, .glb or .stl.");
187 ✗ nb::class_<SceneNode>(m, "SceneNode",
188 "An instance of a scene mesh: scaled per axis, then moved by `pose`.")
189 44 .def(
190 "__init__",
191 46 [](SceneNode* self, std::size_t mesh, Vec3d scale, const RigidTransform& pose) {
192 2 new (self) SceneNode{mesh, scale, pose};
193 2 },
194
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
176 "mesh"_a, "scale"_a = Vec3d{1.0, 1.0, 1.0}, "pose"_a = RigidTransform{})
195 44 .def_rw("mesh", &SceneNode::mesh)
196 .def_rw("pose", &SceneNode::pose);
197
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
44 m.def("write_scene", &WriteScene, "path"_a, "meshes"_a, "nodes"_a,
198 "Writes `meshes` (SceneMesh) drawn once per node: a glTF scene storing each mesh once, "
199 "an OBJ with one object per node, or one merged STL mesh.");
200 44 }
201
202 44 void BindIcq(nb::module_& m) {
203 44 nb::class_<IcqModel>(m, "IcqModel",
204 "A Gaskell implicitly connected quadrilateral (ICQ) shape model, in "
205 "metres: vertices v(I, J, F) on six cube faces, I and J from 0 to Q.")
206 88 .def(nb::init<>())
207 44 .def_rw("q", &IcqModel::q)
208 44 .def_rw("vertices", &IcqModel::vertices,
209 "6 (Q + 1)^2 vertices (x, y, z) in file order: face, then J, then I.")
210
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 .def("index", &IcqModel::Index, "i"_a, "j"_a, "face"_a,
211 "The index of v(i, j, face) in `vertices`, face from 1 to 6.")
212 44 .def("to_radial_shape", &ToRadialShape,
213 "The model as a RadialShape of the same Q: exact when every vertex lies along its "
214 "label's direction, resampled along rays from the origin otherwise.")
215 88 .def("to_mesh", &IcqToMesh,
216 "The model as 12 Q^2 outward triangles over its 6 Q^2 + 2 distinct vertices.")
217 44 .def("__eq__", [](const IcqModel& a, const IcqModel& b) { return a == b; });
218
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
88 m.def("read_icq", &ReadIcq, "path"_a, "metres_per_unit"_a = 1000.0,
219 "Reads an ICQ file; Gaskell's models are in kilometres.");
220
4/4
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
88 m.def("write_icq", &WriteIcq, "path"_a, "model"_a, "metres_per_unit"_a = 1000.0,
221 "Writes an ICQ file; Gaskell's models are in kilometres.");
222
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("to_icq", &ToIcq, "shape"_a, "The RadialShape as an ICQ model.");
223
5/5
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
✓ Branch 13 taken 44 times.
44 m.def("icq_direction", &IcqDirection, "q"_a, "i"_a, "j"_a, "face"_a,
224 "The unit direction of ICQ label (i, j, face) with Gaskell's face orientation.");
225
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("shape_model_from_mesh", &ShapeModelFromMesh, "mesh"_a, "q"_a,
226 "The RadialShape of `q` intervals per face edge of a closed mesh around its origin: the "
227 "outermost crossing of a ray from the origin through each label.",
228 44 nb::call_guard<nb::gil_scoped_release>());
229 44 }
230
231 } // namespace
232
233 44 void BindMesh(nb::module_& m) {
234 44 BindRigidTransform(m);
235 44 BindScene(m);
236 44 BindTriangleMesh(m);
237 44 BindIcq(m);
238 44 }
239
240 } // namespace terraingen::python
241