python/src/boulders.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "bindings.h" | ||
| 2 | #include "common.h" | ||
| 3 | #include "objects.h" | ||
| 4 | #include "tables.h" | ||
| 5 | |||
| 6 | #include "terraingen/layers/boulders/boulder.h" | ||
| 7 | #include "terraingen/layers/boulders/boulder_layer.h" | ||
| 8 | #include "terraingen/layers/boulders/boulder_models.h" | ||
| 9 | #include "terraingen/layers/boulders/boulder_options.h" | ||
| 10 | #include "terraingen/layers/boulders/boulder_shape.h" | ||
| 11 | #include "terraingen/layers/sand_shadow.h" | ||
| 12 | |||
| 13 | #include <algorithm> | ||
| 14 | #include <memory> | ||
| 15 | #include <utility> | ||
| 16 | #include <vector> | ||
| 17 | |||
| 18 | namespace terraingen::python { | ||
| 19 | |||
| 20 | namespace { | ||
| 21 | |||
| 22 | 44 | void BindBoulderStyle(nb::module_& m) { | |
| 23 | 44 | nb::class_<ValueRange>(m, "ValueRange", "An interval values are drawn uniformly from.") | |
| 24 | 44 | .def( | |
| 25 | "__init__", | ||
| 26 | 7 | [](ValueRange* self, double min, double max) { new (self) ValueRange{min, max}; }, | |
| 27 | 132 | "min"_a = 0.0, "max"_a = 0.0) | |
| 28 | 44 | .def_rw("min", &ValueRange::min) | |
| 29 | .def_rw("max", &ValueRange::max); | ||
| 30 | auto style = nb::class_<BoulderStyle>( | ||
| 31 | m, "BoulderStyle", | ||
| 32 | "One kind of boulder: its `solid`, stretched to axes 1 : `intermediate` : `short_axis`, " | ||
| 33 | "rounded toward an ellipsoid by `rounding`, roughened by noise and cut by fracture planes; " | ||
| 34 | 44 | "`weight` is its share of boulders."); | |
| 35 | 44 | BindKwargsInit<BoulderStyle>(style); | |
| 36 | 44 | style.def_rw("name", &BoulderStyle::name) | |
| 37 | 44 | .def_rw("weight", &BoulderStyle::weight) | |
| 38 | 44 | .def_rw("solid", &BoulderStyle::solid) | |
| 39 | 44 | .def_rw("intermediate", &BoulderStyle::intermediate) | |
| 40 | 44 | .def_rw("short_axis", &BoulderStyle::shortAxis) | |
| 41 | 44 | .def_rw("rounding", &BoulderStyle::rounding) | |
| 42 | 44 | .def_rw("min_fractures", &BoulderStyle::minFractures) | |
| 43 | 44 | .def_rw("max_fractures", &BoulderStyle::maxFractures) | |
| 44 | 44 | .def_rw("fracture_depth", &BoulderStyle::fractureDepth) | |
| 45 | 44 | .def_rw("max_fracture_tilt", &BoulderStyle::maxFractureTilt) | |
| 46 | 44 | .def_rw("noise_amplitude", &BoulderStyle::noiseAmplitude) | |
| 47 | 44 | .def_rw("noise_wavelength", &BoulderStyle::noiseWavelength) | |
| 48 | 44 | .def_rw("noise_octaves", &BoulderStyle::noiseOctaves) | |
| 49 | 44 | .def_rw("edge_softness", &BoulderStyle::edgeSoftness) | |
| 50 | .def_rw("face_roughness", &BoulderStyle::faceRoughness); | ||
| 51 | 44 | m.def("default_boulder_styles", &DefaultBoulderStyles, | |
| 52 | "The default mix: blocks and slabs, rounded boulders, wedges, pyramids and shards."); | ||
| 53 | 44 | } | |
| 54 | |||
| 55 | 44 | void BindBoulderShapes(nb::module_& m) { | |
| 56 | 44 | BindBoulderStyle(m); | |
| 57 | auto options = nb::class_<BoulderShapeOptions>( | ||
| 58 | m, "BoulderShapeOptions", | ||
| 59 | 44 | "Boulder shapes: the `styles` to draw from, `subdivisions` of each solid and a `seed`."); | |
| 60 | 44 | BindKwargsInit<BoulderShapeOptions>(options); | |
| 61 | 44 | options.def_rw("styles", &BoulderShapeOptions::styles) | |
| 62 | 44 | .def_rw("subdivisions", &BoulderShapeOptions::subdivisions) | |
| 63 | 44 | .def_rw("weathering", &BoulderShapeOptions::weathering) | |
| 64 | 44 | .def_rw("seed", &BoulderShapeOptions::seed) | |
| 65 | .def_prop_rw( | ||
| 66 | "library", | ||
| 67 | 45 | [](const BoulderShapeOptions& o) { | |
| 68 | 1 | std::vector<SceneMesh> rocks; | |
| 69 | 2 | for (const auto& rock : o.library) { | |
| 70 | 1 | rocks.push_back(*rock); | |
| 71 | } | ||
| 72 | 1 | return rocks; | |
| 73 | ✗ | }, | |
| 74 | 44 | [](BoulderShapeOptions& o, const std::vector<SceneMesh>& rocks) { | |
| 75 | 1 | o.library.clear(); | |
| 76 | 2 | for (const SceneMesh& rock : rocks) { | |
| 77 | 1 | o.library.push_back(std::make_shared<const SceneMesh>(rock)); | |
| 78 | } | ||
| 79 | 1 | }, | |
| 80 | "Rock meshes (SceneMesh, such as `read_mesh` gives) that replace the styles when not " | ||
| 81 | "empty, each shape seed picking one."); | ||
| 82 | 88 | nb::class_<BoulderShape>(m, "BoulderShape", | |
| 83 | "A boulder shape with a long axis of 1 along x, `intermediate` along " | ||
| 84 | "y and `short_axis` along z.") | ||
| 85 | 44 | .def_ro("mesh", &BoulderShape::mesh) | |
| 86 | 44 | .def_ro("style", &BoulderShape::style) | |
| 87 | 44 | .def_ro("material", &BoulderShape::material) | |
| 88 | 44 | .def_ro("intermediate", &BoulderShape::intermediate) | |
| 89 | .def_ro("short_axis", &BoulderShape::shortAxis); | ||
| 90 | 44 | m.def("normalized_boulder_shape", &NormalizedBoulderShape, "mesh"_a, | |
| 91 | "A rock mesh in the unit form of BoulderShape: centred, its longer horizontal side along " | ||
| 92 | "x and 1 long."); | ||
| 93 | 44 | m.def("generate_boulder_shape", &GenerateBoulderShape, "shape_seed"_a, | |
| 94 | 88 | "options"_a = BoulderShapeOptions{}, | |
| 95 | "The shape `shape_seed` gives: the same seed always gives the same shape."); | ||
| 96 | 88 | m.def("boulder_shape_seed", &BoulderShapeSeed, "id"_a, "prototypes"_a = 0, | |
| 97 | "The shape seed of boulder `id`, or of one of `prototypes` shared shapes."); | ||
| 98 | 44 | m.def("validate_boulder_shape_options", &ValidateBoulderShapeOptions, "options"_a, | |
| 99 | kValidateDoc); | ||
| 100 | 44 | } | |
| 101 | |||
| 102 | 44 | void BindBoulder(nb::module_& m) { | |
| 103 | 88 | nb::enum_<BoulderRest>(m, "BoulderRest", "Which of its axes a boulder stands along.") | |
| 104 | 44 | .value("FACE", BoulderRest::Face) | |
| 105 | 44 | .value("SIDE", BoulderRest::Side) | |
| 106 | .value("END", BoulderRest::End); | ||
| 107 | 44 | nb::class_<BoulderFacet>(m, "BoulderFacet", | |
| 108 | "A ventifact facet: facing `azimuth` (radians from east), sloping " | ||
| 109 | "`angle` radians, meeting the ground `reach` metres from the centre.") | ||
| 110 | 44 | .def( | |
| 111 | "__init__", | ||
| 112 | ✗ | [](BoulderFacet* self, double azimuth, double angle, double reach) { | |
| 113 | 1 | new (self) BoulderFacet{azimuth, angle, reach}; | |
| 114 | 1 | }, | |
| 115 | 44 | "azimuth"_a, "angle"_a, "reach"_a) | |
| 116 | 44 | .def_rw("azimuth", &BoulderFacet::azimuth) | |
| 117 | 44 | .def_rw("angle", &BoulderFacet::angle) | |
| 118 | 44 | .def_rw("reach", &BoulderFacet::reach) | |
| 119 | .def( | ||
| 120 | "height_at", | ||
| 121 | 45 | [](const BoulderFacet& f, double east, double north) { | |
| 122 | 1 | return f.HeightAt({east, north}); | |
| 123 | }, | ||
| 124 | 88 | "east"_a, "north"_a, | |
| 125 | "The facet's height (m) above the ground east and north metres from the centre."); | ||
| 126 | ✗ | nb::class_<Boulder>(m, "Boulder", "One boulder: centre, size, burial, orientation and ID.") | |
| 127 | 44 | .def( | |
| 128 | "__init__", | ||
| 129 | 49 | [](Boulder* self, double x, double y, double diameter, double height, double burial, | |
| 130 | double yaw, double lean, std::uint64_t id) { | ||
| 131 | 5 | new (self) Boulder{x, y, diameter, height, burial, yaw, lean, id}; | |
| 132 | 5 | }, | |
| 133 | 220 | "x"_a, "y"_a, "diameter"_a, "height"_a, "burial"_a = 0.0, "yaw"_a = 0.0, "lean"_a = 0.0, | |
| 134 | 88 | "id"_a = 0) | |
| 135 | 44 | .def_rw("x", &Boulder::x) | |
| 136 | 44 | .def_rw("y", &Boulder::y) | |
| 137 | 44 | .def_rw("diameter", &Boulder::diameter) | |
| 138 | 44 | .def_rw("height", &Boulder::height) | |
| 139 | 44 | .def_rw("burial", &Boulder::burial) | |
| 140 | 44 | .def_rw("yaw", &Boulder::yaw) | |
| 141 | 44 | .def_rw("lean", &Boulder::lean) | |
| 142 | 44 | .def_rw("id", &Boulder::id) | |
| 143 | 44 | .def_rw("ground", &Boulder::ground) | |
| 144 | 44 | .def_rw("model", &Boulder::model) | |
| 145 | 44 | .def_rw("wear", &Boulder::wear) | |
| 146 | 44 | .def_rw("rest", &Boulder::rest) | |
| 147 | 44 | .def_rw("tilt", &Boulder::tilt) | |
| 148 | 44 | .def_rw("tilt_azimuth", &Boulder::tiltAzimuth) | |
| 149 | 44 | .def_rw("parent", &Boulder::parent) | |
| 150 | 44 | .def_rw("piece", &Boulder::piece) | |
| 151 | 44 | .def_prop_rw( | |
| 152 | "facets", | ||
| 153 | 2 | [](const Boulder& b) { | |
| 154 | 4 | return std::vector<BoulderFacet>(b.facets.begin(), b.facets.begin() + b.facetCount); | |
| 155 | }, | ||
| 156 | 45 | [](Boulder& b, const std::vector<BoulderFacet>& facets) { | |
| 157 | 2 | if (facets.size() > b.facets.size()) { | |
| 158 | ✗ | throw nb::value_error("A boulder has at most 2 facets"); | |
| 159 | } | ||
| 160 | 1 | std::copy(facets.begin(), facets.end(), b.facets.begin()); | |
| 161 | 1 | b.facetCount = static_cast<std::uint8_t>(facets.size()); | |
| 162 | 1 | }, | |
| 163 | "Its ventifact facets, up to 2.") | ||
| 164 | 88 | .def_prop_ro("exposed_height", &Boulder::ExposedHeight); | |
| 165 | 44 | } | |
| 166 | |||
| 167 | 44 | void BindPatchesAndEjecta(nb::module_& m) { | |
| 168 | auto patches = nb::class_<BoulderPatches>( | ||
| 169 | m, "BoulderPatches", | ||
| 170 | "Patches of richer and poorer boulder cover, about `size` metres across: the abundance " | ||
| 171 | 44 | "varies from 1 / `spread` to `spread` times its mean (1 for the same everywhere)."); | |
| 172 | 44 | BindKwargsInit<BoulderPatches>(patches); | |
| 173 | 44 | patches.def_rw("spread", &BoulderPatches::spread).def_rw("size", &BoulderPatches::size); | |
| 174 | 88 | nb::enum_<BoulderClusterModel>(m, "BoulderClusterModel", | |
| 175 | "How boulder cluster centres are laid out and combine.") | ||
| 176 | 44 | .value("NEAREST", BoulderClusterModel::Nearest) | |
| 177 | .value("THOMAS", BoulderClusterModel::Thomas); | ||
| 178 | 88 | nb::enum_<EjectaProfile>(m, "EjectaProfile", | |
| 179 | "How ejecta boulders fall off from a crater's rim.") | ||
| 180 | 44 | .value("SMOOTHSTEP", EjectaProfile::Smoothstep) | |
| 181 | .value("EXPONENTIAL", EjectaProfile::Exponential); | ||
| 182 | 88 | nb::enum_<EjectaAging>(m, "EjectaAging", "How ejecta boulders fade as their crater ages.") | |
| 183 | 44 | .value("FRESHNESS", EjectaAging::Freshness) | |
| 184 | .value("AGE", EjectaAging::Age); | ||
| 185 | auto ejecta = nb::class_<BoulderEjecta>( | ||
| 186 | m, "BoulderEjecta", | ||
| 187 | "Boulders thrown out by craters at least `min_crater_diameter` across (or deep enough to " | ||
| 188 | "reach bedrock under `regolith_thickness`): extra `coverage` near the rim, falling off " | ||
| 189 | 44 | "to `reach` radii, fading as the crater ages."); | |
| 190 | 44 | BindKwargsInit<BoulderEjecta>(ejecta); | |
| 191 | 44 | ejecta.def_rw("min_crater_diameter", &BoulderEjecta::minCraterDiameter) | |
| 192 | 44 | .def_rw("regolith_thickness", &BoulderEjecta::regolithThickness) | |
| 193 | 44 | .def_rw("excavation_depth_ratio", &BoulderEjecta::excavationDepthRatio) | |
| 194 | 44 | .def_rw("distribution", &BoulderEjecta::distribution) | |
| 195 | 44 | .def_rw("coverage", &BoulderEjecta::coverage) | |
| 196 | 44 | .def_rw("profile", &BoulderEjecta::profile) | |
| 197 | 44 | .def_rw("reach", &BoulderEjecta::reach) | |
| 198 | 44 | .def_rw("decay_radii", &BoulderEjecta::decayRadii) | |
| 199 | 44 | .def_rw("floor_share", &BoulderEjecta::floorShare) | |
| 200 | 44 | .def_rw("aging", &BoulderEjecta::aging) | |
| 201 | 44 | .def_rw("min_freshness", &BoulderEjecta::minFreshness) | |
| 202 | 44 | .def_rw("lifetime_gyr", &BoulderEjecta::lifetimeGyr) | |
| 203 | .def_rw("aging_exponent", &BoulderEjecta::agingExponent); | ||
| 204 | 44 | } | |
| 205 | |||
| 206 | 44 | void BindClustersAndFragments(nb::module_& m) { | |
| 207 | auto clusters = nb::class_<BoulderClusters>( | ||
| 208 | m, "BoulderClusters", | ||
| 209 | "Boulders between craters gathered in clusters: `share` of them crowd within about " | ||
| 210 | "`radius` metres of cluster centres about `spacing` metres apart; the mean density is " | ||
| 211 | 44 | "kept."); | |
| 212 | 44 | BindKwargsInit<BoulderClusters>(clusters); | |
| 213 | 44 | clusters.def_rw("share", &BoulderClusters::share) | |
| 214 | 44 | .def_rw("spacing", &BoulderClusters::spacing) | |
| 215 | 44 | .def_rw("radius", &BoulderClusters::radius) | |
| 216 | .def_rw("model", &BoulderClusters::model); | ||
| 217 | 44 | m.def("validate_boulder_clusters", &ValidateBoulderClusters, "clusters"_a, kValidateDoc); | |
| 218 | 44 | m.def("mean_patch_abundance", &MeanPatchAbundance, "patches"_a, | |
| 219 | "The mean of the patches' abundance, which a density map divides them by."); | ||
| 220 | 44 | m.def("max_cluster_abundance", &MaxClusterAbundance, "clusters"_a, | |
| 221 | "The most a cluster field multiplies the scattered boulder density by."); | ||
| 222 | auto fragmentation = nb::class_<BoulderFragmentation>( | ||
| 223 | m, "BoulderFragmentation", | ||
| 224 | "Boulders broken by wear into pieces that replace them: from `split_wear` boulders at " | ||
| 225 | "least `min_diameter` across split into `min_pieces` to `max_pieces` fitting pieces, " | ||
| 226 | "their cracks opening to `gap_ratio` diameters; from `rubble_wear` the pieces scatter up " | ||
| 227 | 44 | "to `spread_ratio` diameters further, turn and sink. A `split_wear` of 1 breaks nothing."); | |
| 228 | 44 | BindKwargsInit<BoulderFragmentation>(fragmentation); | |
| 229 | 44 | fragmentation.def_rw("min_diameter", &BoulderFragmentation::minDiameter) | |
| 230 | 44 | .def_rw("split_wear", &BoulderFragmentation::splitWear) | |
| 231 | 44 | .def_rw("rubble_wear", &BoulderFragmentation::rubbleWear) | |
| 232 | 44 | .def_rw("min_pieces", &BoulderFragmentation::minPieces) | |
| 233 | 44 | .def_rw("max_pieces", &BoulderFragmentation::maxPieces) | |
| 234 | 44 | .def_rw("gap_ratio", &BoulderFragmentation::gapRatio) | |
| 235 | 44 | .def_rw("spread_ratio", &BoulderFragmentation::spreadRatio) | |
| 236 | 44 | .def_rw("slab_share", &BoulderFragmentation::slabShare) | |
| 237 | 44 | .def_prop_ro("enabled", &BoulderFragmentation::Enabled); | |
| 238 | 44 | m.def("validate_boulder_fragmentation", &ValidateBoulderFragmentation, "fragmentation"_a, | |
| 239 | kValidateDoc); | ||
| 240 | 44 | } | |
| 241 | |||
| 242 | 44 | void BindFillet(nb::module_& m) { | |
| 243 | auto fillet = nb::class_<BoulderFillet>( | ||
| 244 | m, "BoulderFillet", | ||
| 245 | "Regolith piled against each boulder's base: `height_share` of its exposed height out to " | ||
| 246 | "its radius, falling to nothing `width_ratio` diameters further out; 0 `height_share` " | ||
| 247 | 44 | "draws none."); | |
| 248 | 44 | BindKwargsInit<BoulderFillet>(fillet); | |
| 249 | 44 | fillet.def_rw("height_share", &BoulderFillet::heightShare) | |
| 250 | .def_rw("width_ratio", &BoulderFillet::widthRatio); | ||
| 251 | 44 | m.def("boulder_fillet_height", &FilletHeight, "boulder"_a, "distance"_a, "fillet"_a, | |
| 252 | "The height (m) of `boulder`'s fillet `distance` metres from its centre."); | ||
| 253 | 44 | m.def("validate_boulder_fillet", &ValidateBoulderFillet, "fillet"_a, kValidateDoc); | |
| 254 | auto ventifacts = nb::class_<BoulderVentifacts>( | ||
| 255 | m, "BoulderVentifacts", | ||
| 256 | "Windward facets planed on `share` of the boulders by wind blowing toward " | ||
| 257 | "`wind_azimuth`, sloping `min_angle` to `max_angle`, with keels on `keel_share` of them " | ||
| 258 | 44 | "toward `second_wind_azimuth`; angles in radians."); | |
| 259 | 44 | BindKwargsInit<BoulderVentifacts>(ventifacts); | |
| 260 | 44 | ventifacts.def_rw("share", &BoulderVentifacts::share) | |
| 261 | 44 | .def_rw("wind_azimuth", &BoulderVentifacts::windAzimuth) | |
| 262 | 44 | .def_rw("min_angle", &BoulderVentifacts::minAngle) | |
| 263 | 44 | .def_rw("max_angle", &BoulderVentifacts::maxAngle) | |
| 264 | 44 | .def_rw("second_wind_azimuth", &BoulderVentifacts::secondWindAzimuth) | |
| 265 | .def_rw("keel_share", &BoulderVentifacts::keelShare); | ||
| 266 | 44 | m.def("validate_boulder_ventifacts", &ValidateBoulderVentifacts, "ventifacts"_a, kValidateDoc); | |
| 267 | 44 | m.def("gale_ventifacts", &GaleVentifacts, | |
| 268 | "The ventifacts of Gale crater's first 100 sols (Bridges et al. 2014; facet slopes " | ||
| 269 | "from Laity and Bridges 2009); the winds are left to the site."); | ||
| 270 | 44 | } | |
| 271 | |||
| 272 | 44 | void BindRendering(nb::module_& m) { | |
| 273 | 88 | nb::enum_<BoulderRelief>(m, "BoulderRelief", "How boulders are drawn into the heights.") | |
| 274 | 44 | .value("DOME", BoulderRelief::Dome) | |
| 275 | 44 | .value("SHAPE", BoulderRelief::Shape) | |
| 276 | .value("NONE", BoulderRelief::None); | ||
| 277 | auto threshold = nb::class_<BoulderModelThreshold>( | ||
| 278 | m, "BoulderModelThreshold", | ||
| 279 | "Which boulders are placed as 3D boulder models: at least `min_diameter` metres across, or " | ||
| 280 | 44 | "`min_samples` samples across, and below `max_diameter` and `max_samples` when set."); | |
| 281 | 44 | BindKwargsInit<BoulderModelThreshold>(threshold); | |
| 282 | 44 | threshold.def_rw("min_diameter", &BoulderModelThreshold::minDiameter) | |
| 283 | 44 | .def_rw("min_samples", &BoulderModelThreshold::minSamples) | |
| 284 | 44 | .def_rw("max_diameter", &BoulderModelThreshold::maxDiameter) | |
| 285 | 44 | .def_rw("max_samples", &BoulderModelThreshold::maxSamples) | |
| 286 | 132 | .def("is_model", &BoulderModelThreshold::IsModel, "diameter"_a, "spacing"_a = 0.0, | |
| 287 | "Whether a boulder of `diameter` metres is a model at `spacing`."); | ||
| 288 | auto range = nb::class_<BoulderReliefRange>( | ||
| 289 | m, "BoulderReliefRange", | ||
| 290 | "Which boulders are drawn into the heights: from `min_diameter` up to `max_diameter` " | ||
| 291 | 44 | "metres, models only with `models`."); | |
| 292 | 44 | BindKwargsInit<BoulderReliefRange>(range); | |
| 293 | 44 | range.def_rw("min_diameter", &BoulderReliefRange::minDiameter) | |
| 294 | 44 | .def_rw("max_diameter", &BoulderReliefRange::maxDiameter) | |
| 295 | 44 | .def_rw("models", &BoulderReliefRange::models) | |
| 296 | 44 | .def("holds", &BoulderReliefRange::Holds, "diameter"_a, "model"_a, | |
| 297 | "Whether a boulder of `diameter` metres is drawn into the heights."); | ||
| 298 | 44 | } | |
| 299 | |||
| 300 | /// @brief The grouped options under their flat config names. | ||
| 301 | 44 | void BindGroupedOptions(nb::class_<BoulderLayerOptions>& options) { | |
| 302 | using O = BoulderLayerOptions; | ||
| 303 | 44 | BindGrouped(options, "coverage", &O::sizes, &BoulderSizes::coverage); | |
| 304 | 44 | BindGrouped(options, "qa", &O::sizes, &BoulderSizes::qConstant); | |
| 305 | 44 | BindGrouped(options, "qb", &O::sizes, &BoulderSizes::qCoverageCoefficient); | |
| 306 | 44 | BindGrouped(options, "distribution", &O::sizes, &BoulderSizes::distribution); | |
| 307 | 44 | BindGrouped(options, "min_diameter", &O::sizes, &BoulderSizes::minDiameter); | |
| 308 | 44 | BindGrouped(options, "max_diameter", &O::sizes, &BoulderSizes::maxDiameter); | |
| 309 | 88 | options.def_rw("seed", &O::seed).def_rw("patches", &O::patches).def_rw("ejecta", &O::ejecta); | |
| 310 | 44 | BindGrouped(options, "exposure", &O::bedrock, &BoulderBedrock::exposure); | |
| 311 | 44 | BindGrouped(options, "bedrock_abundance", &O::bedrock, &BoulderBedrock::abundance); | |
| 312 | 44 | BindGrouped(options, "exposure_spacing", &O::bedrock, &BoulderBedrock::exposureSpacing); | |
| 313 | 44 | BindGrouped(options, "bedrock_mean_size", &O::bedrock, &BoulderBedrock::meanSize); | |
| 314 | 44 | BindGrouped(options, "min_height_ratio", &O::pose, &BoulderPose::minHeightRatio); | |
| 315 | 44 | BindGrouped(options, "max_height_ratio", &O::pose, &BoulderPose::maxHeightRatio); | |
| 316 | 44 | BindGrouped(options, "min_burial", &O::pose, &BoulderPose::minBurial); | |
| 317 | 44 | BindGrouped(options, "max_burial", &O::pose, &BoulderPose::maxBurial); | |
| 318 | 44 | BindGrouped(options, "max_lean", &O::pose, &BoulderPose::maxLean); | |
| 319 | 44 | BindGrouped(options, "burial_by_wear", &O::pose, &BoulderPose::burialByWear); | |
| 320 | 44 | BindGrouped(options, "side_share", &O::pose, &BoulderPose::sideShare); | |
| 321 | 44 | BindGrouped(options, "end_share", &O::pose, &BoulderPose::endShare); | |
| 322 | 44 | BindGrouped(options, "max_tilt", &O::pose, &BoulderPose::maxTilt); | |
| 323 | 44 | BindGrouped(options, "follow_slope", &O::pose, &BoulderPose::followSlope); | |
| 324 | 44 | BindGrouped(options, "settle", &O::pose, &BoulderPose::settle); | |
| 325 | 44 | BindGrouped(options, "max_slope", &O::pose, &BoulderPose::maxSlope); | |
| 326 | 44 | BindGrouped(options, "roll_distance", &O::pose, &BoulderPose::rollDistance); | |
| 327 | options.def_rw("fillet", &O::fillet); | ||
| 328 | 44 | options.def_rw("density_map", &O::densityMap) | |
| 329 | 44 | .def_rw("slope_map", &O::slopeMap) | |
| 330 | 44 | .def_rw("manual", &O::manual, "Catalog boulders placed as given, as craters.") | |
| 331 | .def_rw("completeness", &O::completeness, | ||
| 332 | "Diameter (m) from which `manual` is a complete catalog at each point."); | ||
| 333 | options.def_rw("ventifacts", &O::ventifacts); | ||
| 334 | 44 | options.def_rw("clusters", &O::clusters).def_rw("fragmentation", &O::fragmentation); | |
| 335 | 44 | BindGrouped(options, "relief", &O::rendering, &BoulderRendering::relief); | |
| 336 | 44 | BindGrouped(options, "models", &O::rendering, &BoulderRendering::models); | |
| 337 | 44 | BindGrouped(options, "relief_range", &O::rendering, &BoulderRendering::reliefRange); | |
| 338 | 44 | BindGrouped(options, "shapes", &O::rendering, &BoulderRendering::shapes); | |
| 339 | 44 | BindGrouped(options, "shape_prototypes", &O::rendering, &BoulderRendering::shapePrototypes); | |
| 340 | options.def_rw("fade", &O::fade); | ||
| 341 | 44 | } | |
| 342 | |||
| 343 | 44 | void BindLayerOptions(nb::module_& m) { | |
| 344 | nb::class_<BoulderLayerOptions> options( | ||
| 345 | m, "BoulderLayerOptions", | ||
| 346 | "A boulder layer's settings: sizes from Golombek and Rapp's model for `coverage` (or " | ||
| 347 | "`distribution`), between `min_diameter` and `max_diameter`; `patches`, `ejecta` and " | ||
| 348 | "`exposure` shape where they lie; heights, burial and lean shape each boulder; `relief` " | ||
| 349 | 44 | "and `models` how they are drawn."); | |
| 350 | 44 | BindKwargsInit<BoulderLayerOptions>(options); | |
| 351 | 44 | BindGroupedOptions(options); | |
| 352 | 44 | m.def("validate_boulder_layer_options", &ValidateBoulderLayerOptions, "options"_a, | |
| 353 | kValidateDoc); | ||
| 354 | 44 | m.def("boulder_throw_distance", &BoulderThrowDistance, "crater_diameter"_a, | |
| 355 | 44 | "regolith_thickness"_a, "gravity"_a, | |
| 356 | "Bart and Melosh's (2010) farthest block throw (m) over a regolith blanket."); | ||
| 357 | 44 | m.def( | |
| 358 | "boulder_candidates", | ||
| 359 | 45 | [](const BoulderLayerOptions& o) { return Mutable(BoulderCandidates(o)); }, "options"_a, | |
| 360 | "The distribution candidate boulders are drawn from before thinning."); | ||
| 361 | 44 | } | |
| 362 | |||
| 363 | 44 | void BindLayer(nb::module_& m) { | |
| 364 | auto boulderLayer = | ||
| 365 | ✗ | nb::class_<BoulderLayer, Layer>( | |
| 366 | m, "BoulderLayer", | ||
| 367 | "Boulders drawn into the heights, the largest optionally " | ||
| 368 | "placed as 3D boulder models; `craters` supplies the craters " | ||
| 369 | "that throw ejecta boulders.") | ||
| 370 | ✗ | .def(nb::init<BoulderLayerOptions, std::shared_ptr<CraterLayer>, Surface>(), | |
| 371 | 132 | "options"_a = BoulderLayerOptions{}, "craters"_a = nb::none(), | |
| 372 | 88 | "surface"_a = Surface{}) | |
| 373 | 44 | .def( | |
| 374 | "boulders", | ||
| 375 | ✗ | [](const BoulderLayer& layer, const Bounds2d& region, double spacing) { | |
| 376 | 6 | return BoulderTable(layer.Boulders(region, spacing)); | |
| 377 | }, | ||
| 378 | 132 | "region"_a, "spacing"_a = 0.0, | |
| 379 | "The boulders over `region` (min_x, min_y, max_x, max_y) as NumPy columns, without " | ||
| 380 | "evaluating any heights or ground.") | ||
| 381 | 44 | .def( | |
| 382 | "abundance_at", | ||
| 383 | ✗ | [](const BoulderLayer& layer, double x, double y, const PyTerrain* terrain, | |
| 384 | std::size_t layers) { | ||
| 385 | const BoulderAbundance abundance = | ||
| 386 | 30437 | WithGround(terrain, layers, | |
| 387 | 30437 | [&](const Ground* g) { return layer.AbundanceAt(x, y, g); }); | |
| 388 | 30437 | return std::pair{abundance.background, abundance.ejecta}; | |
| 389 | }, | ||
| 390 | 176 | "x"_a, "y"_a, "terrain"_a = nb::none(), "layers"_a = 0, | |
| 391 | "The boulder abundance at (x, y): (background, ejecta). With the exposure field, " | ||
| 392 | "give " | ||
| 393 | "the ground beneath the boulders: `terrain`'s source and its first `layers` " | ||
| 394 | "layers.") | ||
| 395 | 44 | .def( | |
| 396 | "density_at", | ||
| 397 | ✗ | [](const BoulderLayer& layer, double diameter, double x, double y, | |
| 398 | const PyTerrain* terrain, std::size_t layers) { | ||
| 399 | 2 | return WithGround(terrain, layers, [&](const Ground* g) { | |
| 400 | 2 | return layer.DensityAt(diameter, x, y, g); | |
| 401 | 2 | }); | |
| 402 | }, | ||
| 403 | 176 | "diameter"_a, "x"_a, "y"_a, "terrain"_a = nb::none(), "layers"_a = 0, | |
| 404 | "Boulders at least `diameter` across per square metre at (x, y).") | ||
| 405 | 44 | .def( | |
| 406 | "unresolved_cover_at", | ||
| 407 | 45 | [](const BoulderLayer& layer, double x, double y, double spacing, | |
| 408 | const PyTerrain* terrain, std::size_t layers) { | ||
| 409 | 1 | const UnresolvedCover cover = WithGround(terrain, layers, [&](const Ground* g) { | |
| 410 | 1 | return layer.UnresolvedCoverAt(x, y, spacing, g); | |
| 411 | }); | ||
| 412 | 1 | return std::pair{cover.mean, cover.variance}; | |
| 413 | }, | ||
| 414 | 176 | "x"_a, "y"_a, "spacing"_a, "terrain"_a = nb::none(), "layers"_a = 0, | |
| 415 | "The share of a sample `spacing` metres across at (x, y) that boulders too small " | ||
| 416 | "to resolve cover: (mean, variance).") | ||
| 417 | 88 | .def_prop_ro("options", &BoulderLayer::Options); | |
| 418 | 44 | BindMaterialCover<BoulderLayer>(boulderLayer); | |
| 419 | 88 | m.def("add_boulder", &AddBoulder, "height"_a, "x"_a, "y"_a, "boulder"_a, "weight"_a = 1.0, | |
| 420 | 132 | "surface"_a = Surface{}, "width"_a = 0.0, | |
| 421 | "`height` at (x, y) with `boulder` drawn into it at `weight`, its dome averaged over " | ||
| 422 | "`width` metres."); | ||
| 423 | 88 | m.def("dome_height", &DomeHeight, "boulder"_a, "distance"_a, "width"_a = 0.0, | |
| 424 | "A boulder's dome `distance` metres from its centre, averaged over `width` metres."); | ||
| 425 | 44 | } | |
| 426 | |||
| 427 | 44 | void BindPlacement(nb::module_& m) { | |
| 428 | 44 | nb::class_<BoulderPlacement>(m, "BoulderPlacement", | |
| 429 | "Where a boulder shape goes: scaled per axis, then posed.") | ||
| 430 | 44 | .def_ro("scale", &BoulderPlacement::scale) | |
| 431 | 44 | .def_ro("pose", &BoulderPlacement::pose) | |
| 432 | 88 | .def("apply", &BoulderPlacement::Apply, "point"_a); | |
| 433 | 44 | m.def("place_boulder_model", &PlaceBoulderModel, "boulder"_a, "shape"_a, "ground_height"_a, | |
| 434 | 88 | "settle"_a = false, | |
| 435 | "The placement of `shape` as `boulder` on ground at `ground_height`; with `settle`, " | ||
| 436 | "tipped over until it rests stably."); | ||
| 437 | 44 | m.def("placement_at_site", &PlacementAtSite, "placement"_a, "boulder"_a, "site"_a, "surface"_a, | |
| 438 | "A boulder's placement moved into the local east-north-up frame of `site`."); | ||
| 439 | 44 | } | |
| 440 | |||
| 441 | 44 | void BindModels(nb::module_& m) { | |
| 442 | 44 | nb::class_<BoulderModel>( | |
| 443 | m, "BoulderModel", "A boulder placed as a 3D model: the boulder, its shape and placement.") | ||
| 444 | 44 | .def_ro("boulder", &BoulderModel::boulder) | |
| 445 | 44 | .def_ro("shape_seed", &BoulderModel::shapeSeed) | |
| 446 | 46 | .def_prop_ro("shape", [](const BoulderModel& b) { return *b.shape; }) | |
| 447 | 44 | .def_ro("placement", &BoulderModel::placement) | |
| 448 | .def_prop_ro( | ||
| 449 | "mesh", | ||
| 450 | 44 | [](const BoulderModel& b) { | |
| 451 | 2 | TriangleMesh placed = b.shape->mesh; | |
| 452 | 518 | for (Vec3d& p : placed.positions) { | |
| 453 | 516 | p = b.placement.Apply(p); | |
| 454 | } | ||
| 455 | 2 | return placed; | |
| 456 | ✗ | }, | |
| 457 | "The boulder's mesh placed in the world."); | ||
| 458 | 44 | m.def("boulder_mesh", &BoulderMesh, "boulder"_a, "options"_a, "ground_height"_a, | |
| 459 | 88 | "prototypes"_a = 0, "`boulder` as a mesh in the world, standing on the ground."); | |
| 460 | 44 | m.def("top_surface", &TopSurface, "placed"_a, "x"_a, "y"_a, | |
| 461 | "The height of a placed mesh's top above (x, y), or None outside it."); | ||
| 462 | 44 | } | |
| 463 | |||
| 464 | 44 | void BindSandShadows(nb::module_& m) { | |
| 465 | auto options = nb::class_<SandShadowOptions>( | ||
| 466 | m, "SandShadowOptions", | ||
| 467 | "Sand tails behind boulders: blown toward `wind_azimuth`, `length_ratio` boulder widths " | ||
| 468 | 44 | "long, flanks at `flank_angle`; angles in radians."); | |
| 469 | 44 | BindKwargsInit<SandShadowOptions>(options); | |
| 470 | 44 | options.def_rw("wind_azimuth", &SandShadowOptions::windAzimuth) | |
| 471 | 44 | .def_rw("length_ratio", &SandShadowOptions::lengthRatio) | |
| 472 | 44 | .def_rw("flank_angle", &SandShadowOptions::flankAngle) | |
| 473 | 44 | .def_rw("min_diameter", &SandShadowOptions::minDiameter) | |
| 474 | 44 | .def_rw("cover_thickness", &SandShadowOptions::coverThickness) | |
| 475 | .def_rw("fade", &SandShadowOptions::fade); | ||
| 476 | 44 | m.def("validate_sand_shadow_options", &ValidateSandShadowOptions, "options"_a, | |
| 477 | "Raises ValueError for invalid options."); | ||
| 478 | auto layer = nb::class_<SandShadowLayer, Layer>(m, "SandShadowLayer", | ||
| 479 | 44 | "Sand tails in the lee of boulders."); | |
| 480 | layer | ||
| 481 | 44 | .def( | |
| 482 | "__init__", | ||
| 483 | ✗ | [](SandShadowLayer* self, std::shared_ptr<BoulderLayer> boulders, | |
| 484 | const SandShadowOptions& shadowOptions, const Surface& surface) { | ||
| 485 | 4 | new (self) SandShadowLayer(std::move(boulders), shadowOptions, surface); | |
| 486 | 1 | }, | |
| 487 | 176 | "boulders"_a, "options"_a = SandShadowOptions{}, "surface"_a = Surface{}) | |
| 488 | 44 | .def( | |
| 489 | "thickness_at", | ||
| 490 | 45 | [](const SandShadowLayer& shadows, double x, double y, double spacing, | |
| 491 | const PyTerrain* terrain, std::size_t layers) { | ||
| 492 | 1 | return WithGround(terrain, layers, [&](const Ground* ground) { | |
| 493 | 1 | return shadows.ThicknessAt(x, y, spacing, ground); | |
| 494 | 1 | }); | |
| 495 | }, | ||
| 496 | 220 | "x"_a, "y"_a, "spacing"_a = 0.0, "terrain"_a = nb::none(), "layers"_a = 0, | |
| 497 | "The sand depth at (x, y) in metres; with the boulders' exposure field, give the " | ||
| 498 | "ground: `terrain`'s source and its first `layers` layers.") | ||
| 499 | 88 | .def_prop_ro("options", &SandShadowLayer::Options); | |
| 500 | 44 | BindMaterialCover<SandShadowLayer>(layer); | |
| 501 | 44 | } | |
| 502 | |||
| 503 | } // namespace | ||
| 504 | |||
| 505 | 44 | void BindBoulders(nb::module_& m) { | |
| 506 | 44 | BindBoulderShapes(m); | |
| 507 | 44 | BindBoulder(m); | |
| 508 | 44 | BindPatchesAndEjecta(m); | |
| 509 | 44 | BindClustersAndFragments(m); | |
| 510 | 44 | BindFillet(m); | |
| 511 | 44 | BindRendering(m); | |
| 512 | 44 | BindLayerOptions(m); | |
| 513 | 44 | BindLayer(m); | |
| 514 | 44 | BindSandShadows(m); | |
| 515 | 44 | BindPlacement(m); | |
| 516 | 44 | BindModels(m); | |
| 517 | 44 | } | |
| 518 | |||
| 519 | } // namespace terraingen::python | ||
| 520 |