feat(editor): add --gen-mesh-pergola garden-arbor primitive

59th procedural mesh primitive. Builds an open-top pergola
from axis-aligned boxes (uses the new addFlatBox helper):

  • 4 corner posts sized so their outer faces line up with
    the footprint perimeter
  • 2 long perimeter beams resting on top of the posts
    along the long edges
  • N cross beams running between the perimeter beams
    (slightly thinner so the lattice has visible crossings)

Distinct from --gen-mesh-canopy because there's no closed
overhead panel — the open lattice top reads as a sun-trellis
or vine-arbor instead of a market-stall awning. Useful for
palace courtyards, druid groves, garden mazes, viewing
arbors, harvest-festival entrance pieces.

Set crossbeams=0 for a bare 4-post + 2-beam frame; default
5 crossbeams gives a pleasing alternating pattern.
This commit is contained in:
Kelsi 2026-05-09 11:26:15 -07:00
parent 6b5f9938a3
commit 6ff5df41eb
3 changed files with 108 additions and 0 deletions

View file

@ -51,6 +51,7 @@ const char* const kArgRequired[] = {
"--gen-mesh-throne", "--gen-mesh-coffin", "--gen-mesh-bookshelf",
"--gen-mesh-tent", "--gen-mesh-firepit", "--gen-mesh-woodpile",
"--gen-mesh-canopy", "--gen-mesh-haystack", "--gen-mesh-dock",
"--gen-mesh-pergola",
"--gen-mesh-table", "--gen-mesh-lamppost", "--gen-mesh-bed",
"--gen-mesh-ladder", "--gen-mesh-well", "--gen-mesh-signpost",
"--gen-mesh-mailbox", "--gen-mesh-tombstone", "--gen-mesh-crate",

View file

@ -6314,6 +6314,110 @@ int handleTent(int& i, int argc, char** argv) {
return 0;
}
int handlePergola(int& i, int argc, char** argv) {
// Pergola: 4 corner posts holding 2 long perimeter beams plus
// N cross-beams running between the long beams. Distinct from
// --gen-mesh-canopy because there's no flat overhead panel —
// the open lattice top reads as a garden arbor / sun-trellis
// rather than a closed-top market stall. The 59th procedural
// mesh primitive.
std::string womBase = argv[++i];
float length = 2.4f;
float width = 1.6f;
float height = 2.2f;
float postR = 0.06f;
float beamT = 0.05f;
int crossbeams = 5;
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { length = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { width = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { height = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { postR = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { beamT = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { crossbeams = std::stoi(argv[++i]); } catch (...) {}
}
if (length <= 0 || width <= 0 || height <= 0 ||
postR <= 0 || postR * 2 >= std::min(length, width) ||
beamT <= 0 || crossbeams < 0 || crossbeams > 32) {
std::fprintf(stderr,
"gen-mesh-pergola: dims > 0; postR*2 < length/width; "
"crossbeams 0..32\n");
return 1;
}
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
wowee::pipeline::WoweeModel wom;
wom.name = std::filesystem::path(womBase).stem().string();
wom.version = 3;
const float L2 = length * 0.5f;
const float W2 = width * 0.5f;
// Posts: full height at the 4 corners, inset by postR so the
// outer face lines up with the (-L2..+L2, -W2..+W2) footprint.
const float postX = L2 - postR;
const float postZ = W2 - postR;
const float postH = height - beamT; // posts stop at beam underside
const float postCY = postH * 0.5f;
const float postHY = postH * 0.5f;
addFlatBox(wom, +postX, postCY, +postZ, postR, postHY, postR);
addFlatBox(wom, -postX, postCY, +postZ, postR, postHY, postR);
addFlatBox(wom, +postX, postCY, -postZ, postR, postHY, postR);
addFlatBox(wom, -postX, postCY, -postZ, postR, postHY, postR);
// Two long perimeter beams along ±Z, spanning the full length
// and sitting on top of the posts.
const float beamCY = postH + beamT * 0.5f;
const float beamHY = beamT * 0.5f;
addFlatBox(wom, 0, beamCY, +postZ, L2, beamHY, postR);
addFlatBox(wom, 0, beamCY, -postZ, L2, beamHY, postR);
// N cross beams running along ±X, spaced evenly between the
// perimeter beams. They sit ON TOP of the perimeter beams so
// the lattice has visible crossings.
if (crossbeams > 0) {
const float xbCY = postH + beamT + beamT * 0.5f;
const float xbHY = beamT * 0.5f;
const float xbHZ = postR * 0.6f; // a bit thinner than perimeter
for (int k = 0; k < crossbeams; ++k) {
float t = (crossbeams == 1) ? 0.5f
: static_cast<float>(k) / (crossbeams - 1);
float cx = -L2 + postR + t * (length - 2.0f * postR);
addFlatBox(wom, cx, xbCY, 0, xbHZ, xbHY, W2);
}
}
wowee::pipeline::WoweeModel::Batch batch;
batch.indexStart = 0;
batch.indexCount = static_cast<uint32_t>(wom.indices.size());
batch.textureIndex = 0;
wom.batches.push_back(batch);
float topY = (crossbeams > 0) ? (postH + 2.0f * beamT) : (postH + beamT);
wom.boundMin = glm::vec3(-L2, 0, -W2);
wom.boundMax = glm::vec3(+L2, topY, +W2);
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"gen-mesh-pergola: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Wrote %s.wom\n", womBase.c_str());
std::printf(" footprint : %.3f x %.3f\n", length, width);
std::printf(" height : %.3f (post %.3f + beam %.3f)\n",
height, postH, beamT);
std::printf(" posts : 4 corners (R=%.3f)\n", postR);
std::printf(" cross beams: %d\n", crossbeams);
std::printf(" vertices : %zu\n", wom.vertices.size());
std::printf(" triangles : %zu\n", wom.indices.size() / 3);
return 0;
}
int handleDock(int& i, int argc, char** argv) {
// Wooden dock / pier: a flat plank deck supported by N pairs
// of square pilings. Distinct from --gen-mesh-bridge which
@ -6953,6 +7057,7 @@ constexpr MeshEntry kMeshTable[] = {
{"--gen-mesh-canopy", 1, handleCanopy},
{"--gen-mesh-haystack", 1, handleHaystack},
{"--gen-mesh-dock", 1, handleDock},
{"--gen-mesh-pergola", 1, handlePergola},
{"--gen-mesh-table", 1, handleTable},
{"--gen-mesh-lamppost", 1, handleLamppost},
{"--gen-mesh-bed", 1, handleBed},

View file

@ -224,6 +224,8 @@ void printUsage(const char* argv0) {
std::printf(" Haystack: terraced stack of N frustums tapering to an apex point (default 0.6/0.9/3/12)\n");
std::printf(" --gen-mesh-dock <wom-base> [length] [width] [height] [pilings/side] [pilingW] [deckT]\n");
std::printf(" Dock: flat plank deck on N pairs of square pilings (default 3.0/1.0/0.6/3/0.10/0.10)\n");
std::printf(" --gen-mesh-pergola <wom-base> [length] [width] [height] [postR] [beamT] [crossbeams]\n");
std::printf(" Pergola: 4 corner posts + 2 perimeter beams + N cross beams (open lattice top, no panel)\n");
std::printf(" --gen-mesh-table <wom-base> [width] [depth] [height] [legThick] [topThick]\n");
std::printf(" Table: flat top slab on 4 corner legs (default 1.6/1.0/0.85/0.10/0.06)\n");
std::printf(" --gen-mesh-lamppost <wom-base> [poleH] [poleT] [baseSize] [lanternSize] [lanternH]\n");