From 6ff5df41ebf7a129f87b325d4096d83c6c53e85a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 11:26:15 -0700 Subject: [PATCH] feat(editor): add --gen-mesh-pergola garden-arbor primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_gen_mesh.cpp | 105 ++++++++++++++++++++++++++++++ tools/editor/cli_help.cpp | 2 + 3 files changed, 108 insertions(+) diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 40b326d1..36bfb9d2 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -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", diff --git a/tools/editor/cli_gen_mesh.cpp b/tools/editor/cli_gen_mesh.cpp index 4d1b472c..36414aba 100644 --- a/tools/editor/cli_gen_mesh.cpp +++ b/tools/editor/cli_gen_mesh.cpp @@ -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(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(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}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 30bd9795..c8d8a777 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -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 [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 [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 [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 [poleH] [poleT] [baseSize] [lanternSize] [lanternH]\n");