feat(editor): add --gen-mesh-bird-bath garden water-feature

77th procedural mesh primitive. Garden bird-bath:

  • stem: thin Y-axis closed cylinder (the column)
  • basin: wider shallow Y-axis closed cylinder on top

Both are full closed cylinders (side wall + ±Y cap fans) so
the model is a watertight solid. Local addYCylinder lambda
emits each tier — keeps the per-handler code self-contained
without forcing yet another module-level helper.

Distinct from --gen-mesh-fountain (basin + spout column,
larger water feature) — bird-bath is the small ornamental
garden version. Useful for monastery courtyards, druid
sanctuaries, noble-house pleasure gardens, sanctuary
clearings, ranger huts.

Watertight under weld (verified 192 manifold edges, 0
boundary, 0 non-manifold).
This commit is contained in:
Kelsi 2026-05-09 13:16:06 -07:00
parent 2a5198df42
commit 234cfd0663
3 changed files with 102 additions and 1 deletions

View file

@ -59,7 +59,7 @@ const char* const kArgRequired[] = {
"--gen-mesh-archery-target", "--gen-mesh-gravel-pile",
"--gen-mesh-stone-bench", "--gen-mesh-mine-cart",
"--gen-mesh-hitching-rail", "--gen-mesh-pillar-row",
"--gen-mesh-statue-base",
"--gen-mesh-statue-base", "--gen-mesh-bird-bath",
"--gen-camp-pack", "--gen-blacksmith-pack", "--gen-village-pack",
"--gen-temple-pack", "--gen-graveyard-pack",
"--gen-garden-pack", "--gen-dock-pack",

View file

@ -5161,6 +5161,104 @@ int handleTent(int& i, int argc, char** argv) {
return 0;
}
int handleBirdBath(int& i, int argc, char** argv) {
// Garden bird-bath: thin cylindrical stem topped by a wide
// shallow disc basin. Distinct from --gen-mesh-fountain
// (basin + spout column, larger water feature) — this is
// the small ornamental garden version. The 77th procedural
// mesh primitive.
std::string womBase = argv[++i];
float stemR = 0.06f; // stem (column) radius
float stemH = 0.85f; // stem height
float basinR = 0.30f; // basin top radius
float basinH = 0.10f; // basin disc thickness
int sides = 16; // cylinder smoothness
parseOptFloat(i, argc, argv, stemR);
parseOptFloat(i, argc, argv, stemH);
parseOptFloat(i, argc, argv, basinR);
parseOptFloat(i, argc, argv, basinH);
parseOptInt(i, argc, argv, sides);
if (stemR <= 0 || stemH <= 0 || basinR <= 0 || basinH <= 0 ||
sides < 6 || sides > 64 || stemR >= basinR) {
std::fprintf(stderr,
"gen-mesh-bird-bath: dims > 0; sides 6..64; "
"stemR < basinR\n");
return 1;
}
stripExt(womBase, ".wom");
wowee::pipeline::WoweeModel wom;
initWomDefaults(wom, womBase);
const float pi = 3.14159265358979f;
// Helper: emit a Y-axis closed cylinder of radius R from
// y = y0 to y = y1.
auto addYCylinder = [&](float R, float y0, float y1) {
// Side wall: ring at y0, ring at y1.
uint32_t bot = static_cast<uint32_t>(wom.vertices.size());
for (int s = 0; s <= sides; ++s) {
float u = static_cast<float>(s) / sides;
float ang = u * 2.0f * pi;
glm::vec3 dir(std::cos(ang), 0.0f, std::sin(ang));
addVertex(wom, {R * dir.x, y0, R * dir.z}, dir, {u, 0});
}
uint32_t top = static_cast<uint32_t>(wom.vertices.size());
for (int s = 0; s <= sides; ++s) {
float u = static_cast<float>(s) / sides;
float ang = u * 2.0f * pi;
glm::vec3 dir(std::cos(ang), 0.0f, std::sin(ang));
addVertex(wom, {R * dir.x, y1, R * dir.z}, dir, {u, 1});
}
for (int s = 0; s < sides; ++s) {
wom.indices.insert(wom.indices.end(), {
bot + s, top + s, bot + s + 1,
bot + s + 1, top + s, top + s + 1
});
}
// Bottom cap (-Y) fan.
uint32_t botCenter = addVertex(wom, {0, y0, 0}, {0, -1, 0},
{0.5f, 0.5f});
uint32_t botRing = static_cast<uint32_t>(wom.vertices.size());
for (int s = 0; s <= sides; ++s) {
float u = static_cast<float>(s) / sides;
float ang = u * 2.0f * pi;
addVertex(wom, {R * std::cos(ang), y0, R * std::sin(ang)},
{0, -1, 0},
{0.5f + 0.5f * std::cos(ang),
0.5f + 0.5f * std::sin(ang)});
}
for (int s = 0; s < sides; ++s) {
wom.indices.insert(wom.indices.end(),
{botCenter, botRing + s + 1, botRing + s});
}
// Top cap (+Y) fan.
uint32_t topCenter = addVertex(wom, {0, y1, 0}, {0, 1, 0},
{0.5f, 0.5f});
uint32_t topRing = static_cast<uint32_t>(wom.vertices.size());
for (int s = 0; s <= sides; ++s) {
float u = static_cast<float>(s) / sides;
float ang = u * 2.0f * pi;
addVertex(wom, {R * std::cos(ang), y1, R * std::sin(ang)},
{0, 1, 0},
{0.5f + 0.5f * std::cos(ang),
0.5f + 0.5f * std::sin(ang)});
}
for (int s = 0; s < sides; ++s) {
wom.indices.insert(wom.indices.end(),
{topCenter, topRing + s, topRing + s + 1});
}
};
addYCylinder(stemR, 0.0f, stemH);
addYCylinder(basinR, stemH, stemH + basinH);
finalizeAsSingleBatch(wom);
setCenteredBoundsXZ(wom, basinR, basinR, stemH + basinH);
if (!saveWomOrError(wom, womBase, "gen-mesh-bird-bath")) return 1;
printWomWrote(womBase);
std::printf(" stem : R=%.3f x %.3f tall\n", stemR, stemH);
std::printf(" basin : R=%.3f x %.3f thick (%d sides)\n",
basinR, basinH, sides);
printWomMeshStats(wom);
return 0;
}
int handleStatueBase(int& i, int argc, char** argv) {
// Statue / monument pedestal: 3-tier stacked-box pedestal
// (plinth foundation, tall body, capital cap). Distinct from
@ -7135,6 +7233,7 @@ constexpr MeshEntry kMeshTable[] = {
{"--gen-mesh-hitching-rail", 1, handleHitchingRail},
{"--gen-mesh-pillar-row", 1, handlePillarRow},
{"--gen-mesh-statue-base", 1, handleStatueBase},
{"--gen-mesh-bird-bath", 1, handleBirdBath},
{"--gen-camp-pack", 1, handleGenCampPack},
{"--gen-blacksmith-pack", 1, handleGenBlacksmithPack},
{"--gen-village-pack", 1, handleGenVillagePack},

View file

@ -296,6 +296,8 @@ void printUsage(const char* argv0) {
std::printf(" Pillar row: N evenly-spaced rectangular pillars with optional square caps (colonnade / temple ruin)\n");
std::printf(" --gen-mesh-statue-base <wom-base> [bodyW] [bodyH] [plinthExtra] [plinthH] [capitalExtra] [capitalH]\n");
std::printf(" Statue base: 3-tier pedestal (plinth + body + capital) for monuments / hero memorials\n");
std::printf(" --gen-mesh-bird-bath <wom-base> [stemR] [stemH] [basinR] [basinH] [sides]\n");
std::printf(" Bird bath: thin cylindrical stem topped by a wide shallow basin disc (small garden water feature)\n");
std::printf(" --gen-camp-pack <outDir>\n");
std::printf(" Convenience: emit tent + firepit + bedroll + canopy + woodpile + haystack into outDir as 6 .wom files\n");
std::printf(" --gen-blacksmith-pack <outDir>\n");