config/feature_lists.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "terraingen/config/build.h" | ||
| 2 | #include "terraingen/config/generate.h" | ||
| 3 | #include "terraingen/mesh/io/mesh_io.h" | ||
| 4 | |||
| 5 | #include <nlohmann/json.hpp> | ||
| 6 | |||
| 7 | #include <array> | ||
| 8 | #include <cstdio> | ||
| 9 | #include <fstream> | ||
| 10 | #include <map> | ||
| 11 | #include <numbers> | ||
| 12 | #include <stdexcept> | ||
| 13 | #include <string> | ||
| 14 | |||
| 15 | namespace terraingen { | ||
| 16 | |||
| 17 | namespace { | ||
| 18 | |||
| 19 | using Json = nlohmann::ordered_json; | ||
| 20 | |||
| 21 | /// `value` with the digits to read it back exactly. | ||
| 22 | 493010 | std::string Number(double value) { | |
| 23 | 493010 | std::array<char, 32> buffer{}; | |
| 24 | 986020 | std::snprintf(buffer.data(), buffer.size(), "%.17g", value); | |
| 25 | 986020 | return buffer.data(); | |
| 26 | } | ||
| 27 | |||
| 28 | /// @throws std::runtime_error if `path` cannot be opened. | ||
| 29 | 72 | std::ofstream OpenForWriting(const std::filesystem::path& path) { | |
| 30 | 72 | std::ofstream file(path, std::ios::binary); | |
| 31 | 72 | if (!file) { | |
| 32 | 2 | throw std::runtime_error("Cannot write " + path.string()); | |
| 33 | } | ||
| 34 | 70 | return file; | |
| 35 | 2 | } | |
| 36 | |||
| 37 | /// @throws std::runtime_error if writing to `file` failed. | ||
| 38 | 70 | void CheckWritten(const std::ofstream& file, const std::filesystem::path& path) { | |
| 39 | 70 | if (!file) { | |
| 40 | 2 | throw std::runtime_error("Cannot write " + path.string()); | |
| 41 | } | ||
| 42 | 68 | } | |
| 43 | |||
| 44 | 36 | void WriteCraterCsv(std::ostream& file, const CraterList& craters, | |
| 45 | const std::vector<CraterAttributes>& attributes) { | ||
| 46 | 36 | const bool detailed = !attributes.empty(); | |
| 47 | file << "x,y,diameter,age_gyr,id,manual" | ||
| 48 | 36 | << (detailed ? ",floor_depth,rim_height,depth_ratio,freshness,complex" : "") << '\n'; | |
| 49 | 53068 | for (std::size_t n = 0; n < craters.Size(); ++n) { | |
| 50 | 53032 | const Crater crater = craters.At(n); | |
| 51 | 106064 | file << Number(crater.x) << ',' << Number(crater.y) << ',' << Number(crater.diameter) << ',' | |
| 52 | 53032 | << Number(crater.ageGyr) << ',' << crater.id << ',' << (crater.manual ? 1 : 0); | |
| 53 | 53032 | if (detailed) { | |
| 54 | 31531 | const CraterAttributes& a = attributes[n]; | |
| 55 | 31531 | file << ',' << Number(a.floorDepth) << ',' << Number(a.rimHeight) << ',' | |
| 56 | 63062 | << Number(a.depthRatio) << ',' << Number(a.freshness) << ',' | |
| 57 | 31531 | << (a.complex ? 1 : 0); | |
| 58 | } | ||
| 59 | 53032 | file << '\n'; | |
| 60 | } | ||
| 61 | 36 | } | |
| 62 | |||
| 63 | 4 | Json CraterJson(const CraterList& craters, const std::vector<CraterAttributes>& attributes) { | |
| 64 | 4 | Json list = Json::array(); | |
| 65 | 38 | for (std::size_t n = 0; n < craters.Size(); ++n) { | |
| 66 | 34 | const Crater crater = craters.At(n); | |
| 67 | Json entry{{"x", crater.x}, | ||
| 68 | {"y", crater.y}, | ||
| 69 | {"diameter", crater.diameter}, | ||
| 70 | {"age_gyr", crater.ageGyr}, | ||
| 71 | {"id", crater.id}, | ||
| 72 | 646 | {"manual", crater.manual}}; | |
| 73 | 34 | if (!attributes.empty()) { | |
| 74 | 32 | const CraterAttributes& a = attributes[n]; | |
| 75 | 32 | entry["floor_depth"] = a.floorDepth; | |
| 76 | 32 | entry["rim_height"] = a.rimHeight; | |
| 77 | 32 | entry["depth_ratio"] = a.depthRatio; | |
| 78 | 32 | entry["freshness"] = a.freshness; | |
| 79 | 32 | entry["complex"] = a.complex; | |
| 80 | } | ||
| 81 | 34 | list.push_back(std::move(entry)); | |
| 82 | 34 | } | |
| 83 | 20 | return Json{{"craters", std::move(list)}}; | |
| 84 | 492 | } | |
| 85 | |||
| 86 | 25 | void WriteBoulderCsv(std::ostream& file, const std::vector<Boulder>& boulders) { | |
| 87 | file << "x,y,ground,diameter,height,burial,yaw,lean,id,model,wear,parent,piece,rest,tilt,tilt_" | ||
| 88 | 25 | "azimuth\n"; | |
| 89 | 14094 | for (const Boulder& boulder : boulders) { | |
| 90 | 28138 | file << Number(boulder.x) << ',' << Number(boulder.y) << ',' | |
| 91 | 28138 | << (boulder.ground ? Number(*boulder.ground) : std::string()) << ',' | |
| 92 | 14069 | << Number(boulder.diameter) << ',' << Number(boulder.height) << ',' | |
| 93 | 14069 | << Number(boulder.burial) << ',' << Number(boulder.yaw) << ',' << Number(boulder.lean) | |
| 94 | 42207 | << ',' << boulder.id << ',' << (boulder.model ? 1 : 0) << ',' << Number(boulder.wear) | |
| 95 | 14069 | << ',' << boulder.parent << ',' << boulder.piece << ',' | |
| 96 | 14069 | << static_cast<int>(boulder.rest) << ',' << Number(boulder.tilt) << ',' | |
| 97 | 14069 | << Number(boulder.tiltAzimuth) << '\n'; | |
| 98 | } | ||
| 99 | 25 | } | |
| 100 | |||
| 101 | 5 | Json BoulderJson(const std::vector<Boulder>& boulders) { | |
| 102 | 5 | Json list = Json::array(); | |
| 103 | 20851 | for (const Boulder& boulder : boulders) { | |
| 104 | 1021454 | list.push_back({{"x", boulder.x}, | |
| 105 | 20846 | {"y", boulder.y}, | |
| 106 | 41692 | {"ground", boulder.ground ? Json(*boulder.ground) : Json(nullptr)}, | |
| 107 | 20846 | {"diameter", boulder.diameter}, | |
| 108 | 20846 | {"height", boulder.height}, | |
| 109 | 20846 | {"burial", boulder.burial}, | |
| 110 | 20846 | {"yaw", boulder.yaw}, | |
| 111 | 20846 | {"lean", boulder.lean}, | |
| 112 | 20846 | {"id", boulder.id}, | |
| 113 | 20846 | {"model", boulder.model}, | |
| 114 | 20846 | {"wear", boulder.wear}, | |
| 115 | 20846 | {"parent", boulder.parent}, | |
| 116 | 20846 | {"piece", boulder.piece}, | |
| 117 | ✗ | {"rest", static_cast<int>(boulder.rest)}, | |
| 118 | 20846 | {"tilt", boulder.tilt}, | |
| 119 | 20846 | {"tilt_azimuth", boulder.tiltAzimuth}}); | |
| 120 | } | ||
| 121 | 25 | return Json{{"boulders", std::move(list)}}; | |
| 122 | 750476 | } | |
| 123 | |||
| 124 | 473 | BoulderPlacement PlacementOf(const BoulderModel& boulder, const Surface& surface, | |
| 125 | const Vec2d& site) { | ||
| 126 | 946 | return PlacementAtSite(boulder.placement, {boulder.boulder.x, boulder.boulder.y}, site, | |
| 127 | 946 | surface); | |
| 128 | } | ||
| 129 | |||
| 130 | /// Writes each shared shape once, with crease normals, and a node per boulder; or, not | ||
| 131 | /// `instanced`, each boulder's placed shape as its own mesh. | ||
| 132 | 19 | void WriteBoulderScene(const std::vector<BoulderModel>& boulders, const std::filesystem::path& path, | |
| 133 | double creaseAngle, const Surface& surface, const Vec2d& site, | ||
| 134 | bool instanced) { | ||
| 135 | 19 | const double radians = creaseAngle * std::numbers::pi / 180.0; | |
| 136 | 19 | std::vector<SceneMesh> meshes; | |
| 137 | 19 | std::map<const BoulderShape*, std::size_t> indices; | |
| 138 | 19 | std::vector<SceneNode> nodes; | |
| 139 | 492 | for (const BoulderModel& boulder : boulders) { | |
| 140 | 473 | if (!instanced) { | |
| 141 | 64 | const BoulderPlacement placement = PlacementOf(boulder, surface, site); | |
| 142 | 64 | TriangleMesh placed = boulder.shape->mesh; | |
| 143 | 14061 | for (Vec3d& p : placed.positions) { | |
| 144 | 13997 | p = placement.Apply(p); | |
| 145 | } | ||
| 146 | 64 | ShadedMesh shaded = WithCreases(placed, radians); | |
| 147 | 64 | nodes.push_back({meshes.size(), {1.0, 1.0, 1.0}, {}}); | |
| 148 | 64 | meshes.push_back( | |
| 149 | 64 | {std::move(shaded.mesh), std::move(shaded.normals), boulder.shape->material}); | |
| 150 | 64 | continue; | |
| 151 | 64 | } | |
| 152 | 409 | auto [found, added] = indices.emplace(boulder.shape.get(), meshes.size()); | |
| 153 | 409 | if (added) { | |
| 154 | 343 | ShadedMesh shaded = WithCreases(boulder.shape->mesh, radians); | |
| 155 | 343 | meshes.push_back( | |
| 156 | 343 | {std::move(shaded.mesh), std::move(shaded.normals), boulder.shape->material}); | |
| 157 | 343 | } | |
| 158 | 409 | const BoulderPlacement placement = PlacementOf(boulder, surface, site); | |
| 159 | 409 | nodes.push_back({found->second, placement.scale, placement.pose}); | |
| 160 | } | ||
| 161 | 19 | WriteScene(path, meshes, nodes); | |
| 162 | 428 | } | |
| 163 | |||
| 164 | } // namespace | ||
| 165 | |||
| 166 | 42 | void WriteCraterList(const CraterList& craters, const std::filesystem::path& path, | |
| 167 | ListFormat format, const std::vector<CraterAttributes>& attributes) { | ||
| 168 | 42 | if (!attributes.empty() && attributes.size() != craters.Size()) { | |
| 169 | 1 | throw std::invalid_argument("Crater attributes must be empty or one per crater"); | |
| 170 | } | ||
| 171 | 41 | std::ofstream file = OpenForWriting(path); | |
| 172 | 40 | switch (format) { | |
| 173 | 36 | case ListFormat::Csv: | |
| 174 | 36 | WriteCraterCsv(file, craters, attributes); | |
| 175 | 36 | break; | |
| 176 | 4 | case ListFormat::Json: | |
| 177 | 4 | file << CraterJson(craters, attributes).dump(2) << '\n'; | |
| 178 | 4 | break; | |
| 179 | } | ||
| 180 | 40 | CheckWritten(file, path); | |
| 181 | 40 | } | |
| 182 | |||
| 183 | 31 | void WriteBoulderList(const std::vector<Boulder>& boulders, const std::filesystem::path& path, | |
| 184 | ListFormat format) { | ||
| 185 | 31 | std::ofstream file = OpenForWriting(path); | |
| 186 | 30 | switch (format) { | |
| 187 | 25 | case ListFormat::Csv: | |
| 188 | 25 | WriteBoulderCsv(file, boulders); | |
| 189 | 25 | break; | |
| 190 | 5 | case ListFormat::Json: | |
| 191 | 5 | file << BoulderJson(boulders).dump(2) << '\n'; | |
| 192 | 5 | break; | |
| 193 | } | ||
| 194 | 30 | CheckWritten(file, path); | |
| 195 | 30 | } | |
| 196 | |||
| 197 | 22 | void WriteBoulderModels(const std::vector<BoulderModel>& boulders, | |
| 198 | const std::filesystem::path& path, double creaseAngle, | ||
| 199 | const Surface& surface, const Vec2d& site, bool instanced) { | ||
| 200 | 22 | if (!(creaseAngle >= 0.0 && creaseAngle <= 180.0)) { | |
| 201 | 2 | throw std::invalid_argument("Crease angle must be between 0 and 180 degrees"); | |
| 202 | } | ||
| 203 | 20 | if (boulders.empty()) { | |
| 204 | 1 | throw std::invalid_argument("There are no boulder models to write"); | |
| 205 | } | ||
| 206 | 19 | WriteBoulderScene(boulders, path, creaseAngle, surface, site, instanced); | |
| 207 | 18 | } | |
| 208 | |||
| 209 | } // namespace terraingen | ||
| 210 |