From 4932947631adae98cabef5bedaf655c477903d4c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 09:20:08 -0700 Subject: [PATCH] feat(editor): add --gen-mesh-weathervane rooftop primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 53rd procedural mesh: 6-box rooftop wind indicator — base plate, tall vertical post, perpendicular N-S and E-W cross arms (cardinal direction markers), a long horizontal arrow on top of the cross, and a small tail box at the back end of the arrow that visually balances the head and gives the arrow its directional read. Useful for farm rooftops, chapel spires, town halls, lighthouse caps, manor turrets — anywhere a fantasy world wants visible wind direction. Defaults to a 1.50m post with 0.40m cross arms and a 0.55m-half-length arrow (~1.67m total). --- tools/editor/cli_gen_mesh.cpp | 133 ++++++++++++++++++++++++++++++++++ tools/editor/cli_help.cpp | 2 + tools/editor/main.cpp | 2 +- 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/tools/editor/cli_gen_mesh.cpp b/tools/editor/cli_gen_mesh.cpp index 350c8cc8..e694929c 100644 --- a/tools/editor/cli_gen_mesh.cpp +++ b/tools/editor/cli_gen_mesh.cpp @@ -4655,6 +4655,136 @@ int handleCoffin(int& i, int argc, char** argv) { return 0; } +int handleWeathervane(int& i, int argc, char** argv) { + // Weathervane: 6-box rooftop wind indicator — base plate, + // tall vertical post, perpendicular N-S and E-W cross arms + // (cardinal direction markers), a long horizontal arrow on + // top of the cross, and a small tail box at the back end of + // the arrow that visually balances the head. Useful for + // farm rooftops, chapel spires, town halls, lighthouse caps. + // The 53rd procedural mesh primitive. + std::string womBase = argv[++i]; + float postHeight = 1.50f; + float postT = 0.05f; + float baseSize = 0.30f; + float armLen = 0.40f; // half-length of each cross arm + float arrowLen = 0.55f; // half-length of the arrow body + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { postHeight = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { postT = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { baseSize = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { armLen = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { arrowLen = std::stof(argv[++i]); } catch (...) {} + } + if (postHeight <= 0 || postT <= 0 || baseSize <= 0 || + armLen <= 0 || arrowLen <= 0 || postT >= baseSize) { + std::fprintf(stderr, + "gen-mesh-weathervane: dims > 0; post must fit in base\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}); + } + }; + // Base plate at floor. + float baseHeight = baseSize * 0.30f; + float halfBase = baseSize * 0.5f; + addBox(0, baseHeight * 0.5f, 0, + halfBase, baseHeight * 0.5f, halfBase); + // Vertical post. + float halfPost = postT * 0.5f; + float poleBottomY = baseHeight; + float poleTopY = baseHeight + postHeight; + float poleCY = (poleBottomY + poleTopY) * 0.5f; + addBox(0, poleCY, 0, halfPost, postHeight * 0.5f, halfPost); + // Cross arms at the top of the post — 2 perpendicular thin + // bars forming the cardinal-direction "+" marker. + float armT = postT * 0.7f; + float halfArmT = armT * 0.5f; + float crossY = poleTopY - armT * 1.0f; + addBox(0, crossY, 0, armLen, halfArmT, halfArmT); // E-W (along X) + addBox(0, crossY, 0, halfArmT, halfArmT, armLen); // N-S (along Z) + // Arrow body on top of the cross — long thin bar that + // would rotate to the wind direction. Aligned along +X by + // default (designers can rotate at placement time). + float arrowY = poleTopY + armT * 0.7f; + float arrowT2 = armT * 1.1f; + float halfAT = arrowT2 * 0.5f; + addBox(0, arrowY, 0, arrowLen, halfAT, halfAT); + // Arrow tail: small box at -X end so the arrow looks + // directional rather than symmetric. + float tailLen = arrowLen * 0.3f; + float tailX = -arrowLen + tailLen; + addBox(tailX, arrowY, 0, halfAT, arrowT2 * 1.4f, halfAT); + 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 = arrowY + arrowT2 * 1.4f; + float maxX = std::max({halfBase, arrowLen, tailX + halfAT}); + wom.boundMin = glm::vec3(-maxX, 0.0f, -armLen); + wom.boundMax = glm::vec3( maxX, totalH, armLen); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-weathervane: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" total H : %.3f\n", totalH); + std::printf(" base : %.3f square × %.3f tall\n", + baseSize, baseHeight); + std::printf(" post : %.3f square × %.3f tall\n", + postT, postHeight); + std::printf(" cross arms : 2 × %.3f half-length (N-S + E-W)\n", armLen); + std::printf(" arrow : %.3f half-length (with tail at -X)\n", + arrowLen); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + int handleBeehive(int& i, int argc, char** argv) { // Beehive: 5-box woven straw skep — stacked tiers of // decreasing width approximating a dome, with a small @@ -6482,6 +6612,9 @@ bool handleGenMesh(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--gen-mesh-beehive") == 0 && i + 1 < argc) { outRc = handleBeehive(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--gen-mesh-weathervane") == 0 && i + 1 < argc) { + outRc = handleWeathervane(i, argc, argv); return true; + } return false; } diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 51ae0d59..433f51af 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -218,6 +218,8 @@ void printUsage(const char* argv0) { std::printf(" Gate: 2 vertical posts + 3 horizontal rails (default 1.80/1.30/0.10/0.06)\n"); std::printf(" --gen-mesh-beehive [baseWidth] [height] [plateH]\n"); std::printf(" Beehive (skep): 4 tapered tiers + entrance notch on +Z face (default 0.70/0.85/0.05)\n"); + std::printf(" --gen-mesh-weathervane [postH] [postT] [baseSize] [armLen] [arrowLen]\n"); + std::printf(" Weathervane: base + post + N-S/E-W cross arms + arrow with tail (default 1.50/0.05/0.30/0.40/0.55)\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 d39954b6..7f8ce60d 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -159,7 +159,7 @@ int main(int argc, char* argv[]) { "--gen-mesh-ladder", "--gen-mesh-well", "--gen-mesh-signpost", "--gen-mesh-mailbox", "--gen-mesh-tombstone", "--gen-mesh-crate", "--gen-mesh-stool", "--gen-mesh-cauldron", "--gen-mesh-gate", - "--gen-mesh-beehive", + "--gen-mesh-beehive", "--gen-mesh-weathervane", "--gen-texture-gradient", "--gen-mesh-from-heightmap", "--export-mesh-heightmap", "--displace-mesh",