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 | if (array.shape(1) != 3) { | |
| 30 | ✗ | throw std::invalid_argument("Expected an array of shape (n, 3)"); | |
| 31 | } | ||
| 32 | 1 | std::vector<Vec3d> points(array.shape(0)); | |
| 33 | 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 | 27 | flat.reserve(3 * vectors.size()); | |
| 42 | 32835 | for (const Vec3d& v : vectors) { | |
| 43 | 32808 | flat.insert(flat.end(), {v.x, v.y, v.z}); | |
| 44 | } | ||
| 45 | 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 | 25 | triangles.reserve(3 * mesh.triangles.size()); | |
| 51 | 62265 | for (const auto& t : mesh.triangles) { | |
| 52 | 62240 | triangles.insert(triangles.end(), t.begin(), t.end()); | |
| 53 | } | ||
| 54 | 50 | return nb::make_tuple(VectorArray(mesh.positions), | |
| 55 | 150 | Column(std::move(triangles)).attr("reshape")(-1, 3)); | |
| 56 | 25 | } | |
| 57 | |||
| 58 | 1 | TriangleMesh MeshFromArrays(const Points& positions, const Indices& triangles) { | |
| 59 | 1 | if (triangles.shape(1) != 3) { | |
| 60 | ✗ | throw std::invalid_argument("Triangles must have shape (m, 3)"); | |
| 61 | } | ||
| 62 | 1 | TriangleMesh mesh; | |
| 63 | 1 | mesh.positions = ToVectors(positions); | |
| 64 | 13 | for (std::size_t i = 0; i < triangles.shape(0); ++i) { | |
| 65 | 12 | mesh.triangles.push_back( | |
| 66 | 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 | 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 | 44 | "rotation"_a = std::array<double, 9>{1, 0, 0, 0, 1, 0, 0, 0, 1}, | |
| 83 | 88 | "translation"_a = Vec3d{}, "`rotation` is a row-major 3 x 3 matrix.") | |
| 84 | 44 | .def_static("rotation_x", &RigidTransform::RotationX, "angle"_a, "Radians.") | |
| 85 | 44 | .def_static("rotation_y", &RigidTransform::RotationY, "angle"_a, "Radians.") | |
| 86 | 44 | .def_static("rotation_z", &RigidTransform::RotationZ, "angle"_a, "Radians.") | |
| 87 | 88 | .def_static("translation_by", &RigidTransform::Translation, "offset"_a) | |
| 88 | 44 | .def_rw("rotation", &RigidTransform::rotation) | |
| 89 | 44 | .def_rw("translation", &RigidTransform::translation) | |
| 90 | 44 | .def("apply", &RigidTransform::Apply, "point"_a) | |
| 91 | 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 | 40 | }, | |
| 102 | 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 | new (self) TriangleMesh(MeshFromArrays(positions, triangles)); | |
| 112 | 1 | }, | |
| 113 | 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 | 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 | 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 | const ShadedMesh shaded = WithCreases(self, creaseAngle); | |
| 137 | 2 | return nb::make_tuple(shaded.mesh, VectorArray(shaded.normals)); | |
| 138 | 1 | }, | |
| 139 | 44 | "crease_angle"_a, | |
| 140 | "(mesh, normals): vertices split along creases sharper than `crease_angle` radians, " | ||
| 141 | "with smooth normals elsewhere.") | ||
| 142 | 44 | .def("transformed", &Transformed, "transform"_a) | |
| 143 | 44 | .def("scaled", &Scaled, "scale"_a, "The mesh scaled per axis.") | |
| 144 | 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 | 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 | 44 | m.def("base_solid_mesh", &BaseSolidMesh, "solid"_a, "The mesh of a base solid."); | |
| 155 | 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 | 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 | 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 | 220 | "mesh"_a, "material"_a = MeshMaterial{}) | |
| 178 | 44 | .def_rw("mesh", &SceneMesh::mesh) | |
| 179 | .def_rw("material", &SceneMesh::material); | ||
| 180 | 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 | 44 | m.def( | |
| 185 | 6 | "is_mesh_path", [](const std::filesystem::path& path) { return IsMeshPath(path); }, | |
| 186 | 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 | 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 | 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 | 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 | 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 | 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 | 44 | m.def("to_icq", &ToIcq, "shape"_a, "The RadialShape as an ICQ model."); | |
| 223 | 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 | 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 |