From b1f6563c0a6366fcd2bb350be45dbe0514a3e3b1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 09:46:59 -0700 Subject: [PATCH] feat(editor): add --gen-mesh-podium ceremony-platform primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 56th procedural mesh: stepped pyramid speaker stand — configurable number of steps (2..8) tapering from base to top with equal step heights for visual rhythm, plus a small lectern box positioned at the back of the top platform so a speaker has room in front. Top platform is half the base footprint, with each step shrinking by an equal increment. Useful for throne rooms, ceremonial dais, NPC speaker positions, monument bases, judgment platforms. Defaults to 1.60m base × 3 steps with a 0.30m lectern (~0.96m total). --- tools/editor/cli_gen_mesh.cpp | 114 ++++++++++++++++++++++++++++++++++ tools/editor/cli_help.cpp | 2 + tools/editor/main.cpp | 1 + 3 files changed, 117 insertions(+) diff --git a/tools/editor/cli_gen_mesh.cpp b/tools/editor/cli_gen_mesh.cpp index ddbe2720..db10d04a 100644 --- a/tools/editor/cli_gen_mesh.cpp +++ b/tools/editor/cli_gen_mesh.cpp @@ -4655,6 +4655,117 @@ int handleCoffin(int& i, int argc, char** argv) { return 0; } +int handlePodium(int& i, int argc, char** argv) { + // Podium: 4-box stepped pyramid speaker stand — large + // bottom step, medium middle step, small top platform, + // and a small lectern box on top of the platform. Useful + // for throne rooms, ceremonies, NPC speaker positions, + // monument bases. The 56th procedural mesh primitive. + std::string womBase = argv[++i]; + float baseSize = 1.60f; // bottom step width = depth + float baseHeight = 0.20f; + int stepCount = 3; // total stepped tiers (incl. top) + float lecternSize = 0.30f; // lectern at the very top + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { baseSize = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { baseHeight = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { stepCount = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { lecternSize = std::stof(argv[++i]); } catch (...) {} + } + if (baseSize <= 0 || baseHeight <= 0 || lecternSize <= 0 || + stepCount < 2 || stepCount > 8) { + std::fprintf(stderr, + "gen-mesh-podium: dims > 0; stepCount 2..8\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; + auto addBox = [&](float cx, float cy, float cz, + float hx, float hy, float hz) { + struct Face { glm::vec3 n, du, dv; }; + Face faces[6] = { + {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, + {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, + {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, + {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, + {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, + {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, + }; + glm::vec3 c(cx, cy, cz); + for (const Face& f : faces) { + glm::vec3 center = c + glm::vec3(f.n.x*hx, f.n.y*hy, f.n.z*hz); + glm::vec3 du = glm::vec3(f.du.x*hx, f.du.y*hy, f.du.z*hz); + glm::vec3 dv = glm::vec3(f.dv.x*hx, f.dv.y*hy, f.dv.z*hz); + uint32_t base = static_cast(wom.vertices.size()); + auto push = [&](glm::vec3 p, float u, float v) { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = f.n; vtx.texCoord = {u, v}; + wom.vertices.push_back(vtx); + }; + push(center - du - dv, 0, 0); + push(center + du - dv, 1, 0); + push(center + du + dv, 1, 1); + push(center - du + dv, 0, 1); + wom.indices.insert(wom.indices.end(), + {base, base + 1, base + 2, base, base + 2, base + 3}); + } + }; + // Each step is shorter on each side by ~15% of base size, + // and the top platform's footprint is half the base. Step + // heights are equal for visual rhythm. + float topSize = baseSize * 0.50f; + float sizeStep = (baseSize - topSize) / (stepCount - 1); + float stepHeight = baseHeight; + for (int s = 0; s < stepCount; ++s) { + float halfSide = (baseSize - s * sizeStep) * 0.5f; + float stepCY = stepHeight * 0.5f + s * stepHeight; + addBox(0, stepCY, 0, halfSide, stepHeight * 0.5f, halfSide); + } + // Lectern: small box on top of the top platform, at the + // back so a speaker has room in front. Faces +Z. + float halfL = lecternSize * 0.5f; + float lecternH = lecternSize * 1.2f; + float platformTopY = stepHeight * stepCount; + float lecternCY = platformTopY + lecternH * 0.5f; + float lecternZ = -topSize * 0.25f; // pushed back + addBox(0, lecternCY, lecternZ, + halfL, lecternH * 0.5f, halfL * 0.4f); + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float totalH = lecternCY + lecternH * 0.5f; + float halfBase = baseSize * 0.5f; + wom.boundMin = glm::vec3(-halfBase, 0.0f, -halfBase); + wom.boundMax = glm::vec3( halfBase, totalH, halfBase); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-podium: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" base : %.3f square × %.3f thick\n", + baseSize, baseHeight); + std::printf(" steps : %d (top %.3f square)\n", stepCount, topSize); + std::printf(" lectern : %.3f wide (at back)\n", lecternSize); + std::printf(" total H : %.3f\n", totalH); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + int handleSundial(int& i, int argc, char** argv) { // Sundial: 8-box garden timekeeper — square base plate at // floor, central vertical gnomon slab spanning the diameter @@ -6862,6 +6973,9 @@ bool handleGenMesh(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--gen-mesh-sundial") == 0 && i + 1 < argc) { outRc = handleSundial(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--gen-mesh-podium") == 0 && i + 1 < argc) { + outRc = handlePodium(i, argc, argv); return true; + } return false; } diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 2c3f3468..b0879763 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -226,6 +226,8 @@ void printUsage(const char* argv0) { std::printf(" Scarecrow: cruciform body + cross arms + head + brimmed hat (default 1.80/1.40/0.06/0.22/0.32)\n"); std::printf(" --gen-mesh-sundial [baseSize] [baseH] [gnomonH] [gnomonT]\n"); std::printf(" Sundial: square base + central gnomon slab + 4 cardinal hour markers (default 0.80/0.06/0.35/0.04)\n"); + std::printf(" --gen-mesh-podium [baseSize] [stepH] [steps] [lecternSize]\n"); + std::printf(" Podium: stepped pyramid + lectern at back of top platform (default 1.60/0.20/3/0.30)\n"); std::printf(" Procedural tree: cylindrical trunk + spherical foliage (default 0.1/2.0/0.7)\n"); std::printf(" --displace-mesh [scale]\n"); std::printf(" Offset each vertex along its normal by heightmap brightness × scale (default 1.0)\n"); diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 24a8f99b..f212e834 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -167,6 +167,7 @@ int main(int argc, char* argv[]) { "--gen-mesh-stool", "--gen-mesh-cauldron", "--gen-mesh-gate", "--gen-mesh-beehive", "--gen-mesh-weathervane", "--gen-mesh-scarecrow", "--gen-mesh-sundial", + "--gen-mesh-podium", "--gen-texture-gradient", "--gen-mesh-from-heightmap", "--export-mesh-heightmap", "--displace-mesh",