From e746e400dd5a2e17bfb080e4c482879a769a9555 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 10:25:38 -0700 Subject: [PATCH] feat(editor): add --gen-mesh-tent A-frame canvas tent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 53rd procedural mesh primitive. Builds a watertight A-frame tent: • two sloped roof panels meeting at a ridge that runs along the X axis • two triangular gables sealing the ends • optional inverted-V door notch carved out of the +X gable (parameterized by doorH and doorW; either set to 0 disables the cutout for a solid gable) • bottom face for collision-bake watertightness Useful set dressing for Horde encampments, troll camps, Defias bandit hideouts, scout/quartermaster overlooks, and generic outdoor quest hubs. Default footprint 1.6 x 1.0 with 0.9 ridge height and a 0.5 x 0.4 door notch — all dimensions overridable on the CLI. --- tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_gen_mesh.cpp | 153 ++++++++++++++++++++++++++++++ tools/editor/cli_help.cpp | 2 + 3 files changed, 156 insertions(+) diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 8edb1cb3..45e9082c 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -49,6 +49,7 @@ const char* const kArgRequired[] = { "--gen-mesh-banner", "--gen-mesh-grave", "--gen-mesh-bench", "--gen-mesh-shrine", "--gen-mesh-totem", "--gen-mesh-cage", "--gen-mesh-throne", "--gen-mesh-coffin", "--gen-mesh-bookshelf", + "--gen-mesh-tent", "--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 da22c8e3..ceabfe1d 100644 --- a/tools/editor/cli_gen_mesh.cpp +++ b/tools/editor/cli_gen_mesh.cpp @@ -7051,6 +7051,156 @@ int handleBookshelf(int& i, int argc, char** argv) { return 0; } +int handleTent(int& i, int argc, char** argv) { + // A-frame canvas tent: ridge running along X from + // (-L/2, H, 0) to (+L/2, H, 0); rectangular footprint LxW + // on the ground; two sloped roof panels meeting at the ridge + // and two triangular gables closing the ends. Optionally a + // simple inverted-V door notch is cut from the +X gable so + // there is a visible entrance. Watertight bottom face is + // included so the model is a closed solid for collision + // baking. The 53rd procedural mesh primitive. + std::string womBase = argv[++i]; + float length = 1.6f; + float width = 1.0f; + float height = 0.9f; + float doorH = 0.5f; + float doorW = 0.4f; + 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 { doorH = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { doorW = std::stof(argv[++i]); } catch (...) {} + } + if (length <= 0 || width <= 0 || height <= 0 || + doorH < 0 || doorH >= height || + doorW < 0 || doorW >= width) { + std::fprintf(stderr, + "gen-mesh-tent: dims > 0; doorH < height; doorW < width\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 addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + const float L2 = length * 0.5f; + const float W2 = width * 0.5f; + // Slope normals for the two roof panels — built from the panel + // edge vectors then normalized so adjacent vertices share the + // same per-face shading. + glm::vec3 nBack = glm::normalize(glm::vec3(0.0f, W2, -height)); + glm::vec3 nFront = glm::normalize(glm::vec3(0.0f, W2, height)); + // Back roof panel (faces -Z and +Y): A=(-L2,0,-W2), B=(+L2,0,-W2), + // R1=(+L2,H,0), R0=(-L2,H,0). Quad → 2 triangles, CCW from outside. + { + uint32_t a = addV({-L2, 0, -W2}, nBack, {0, 0}); + uint32_t b = addV({+L2, 0, -W2}, nBack, {1, 0}); + uint32_t r1 = addV({+L2, height, 0}, nBack, {1, 1}); + uint32_t r0 = addV({-L2, height, 0}, nBack, {0, 1}); + wom.indices.insert(wom.indices.end(), {a, b, r1, a, r1, r0}); + } + // Front roof panel (faces +Z and +Y). + { + uint32_t d = addV({-L2, 0, +W2}, nFront, {0, 0}); + uint32_t r0 = addV({-L2, height, 0}, nFront, {0, 1}); + uint32_t r1 = addV({+L2, height, 0}, nFront, {1, 1}); + uint32_t c = addV({+L2, 0, +W2}, nFront, {1, 0}); + wom.indices.insert(wom.indices.end(), {d, r0, r1, d, r1, c}); + } + // -X gable (full triangle, no door): A=(-L2,0,-W2), R0=(-L2,H,0), + // D=(-L2,0,+W2). Faces -X. + { + glm::vec3 n(-1, 0, 0); + uint32_t a = addV({-L2, 0, -W2}, n, {0, 0}); + uint32_t r0 = addV({-L2, height, 0}, n, {0.5f, 1}); + uint32_t d = addV({-L2, 0, +W2}, n, {1, 0}); + wom.indices.insert(wom.indices.end(), {a, r0, d}); + } + // +X gable: B=(+L2,0,-W2), C=(+L2,0,+W2), R1=(+L2,H,0). Faces +X. + // If doorH>0 we carve out a tapered notch — bottom edge of width + // doorW, apex on the centerline at height doorH — and replace the + // single gable triangle with a 4-triangle fan around the door. + { + glm::vec3 n(+1, 0, 0); + if (doorH > 0 && doorW > 0) { + uint32_t b = addV({+L2, 0, -W2}, n, {0, 0}); + uint32_t bl = addV({+L2, 0, -doorW * 0.5f}, n, + {0.5f - doorW / (2 * width), 0}); + uint32_t br = addV({+L2, 0, +doorW * 0.5f}, n, + {0.5f + doorW / (2 * width), 0}); + uint32_t c = addV({+L2, 0, +W2}, n, {1, 0}); + uint32_t r1 = addV({+L2, height, 0}, n, {0.5f, 1}); + uint32_t dt = addV({+L2, doorH, 0}, n, + {0.5f, doorH / height}); + // Slab right of the door, slab left of the door, then the + // peak triangle bridging door-top to ridge. + wom.indices.insert(wom.indices.end(), {b, c, br}); + wom.indices.insert(wom.indices.end(), {b, br, dt}); + wom.indices.insert(wom.indices.end(), {b, dt, r1}); + wom.indices.insert(wom.indices.end(), {c, r1, dt}); + wom.indices.insert(wom.indices.end(), {c, dt, br}); + (void)bl; // left-base slot reserved for symmetric door variant + } else { + uint32_t b = addV({+L2, 0, -W2}, n, {0, 0}); + uint32_t c = addV({+L2, 0, +W2}, n, {1, 0}); + uint32_t r1 = addV({+L2, height, 0}, n, {0.5f, 1}); + wom.indices.insert(wom.indices.end(), {b, c, r1}); + } + } + // Ground face (faces -Y) so the tent is a closed solid for + // collision baking. + { + glm::vec3 n(0, -1, 0); + uint32_t a = addV({-L2, 0, -W2}, n, {0, 0}); + uint32_t b = addV({+L2, 0, -W2}, n, {1, 0}); + uint32_t c = addV({+L2, 0, +W2}, n, {1, 1}); + uint32_t d = addV({-L2, 0, +W2}, n, {0, 1}); + wom.indices.insert(wom.indices.end(), {a, d, c, a, c, b}); + } + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + wom.boundMin = glm::vec3(-L2, 0, -W2); + wom.boundMax = glm::vec3(+L2, height, +W2); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-tent: 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 (ridge along X)\n", height); + if (doorH > 0 && doorW > 0) { + std::printf(" door : H=%.3f W=%.3f on +X gable\n", + doorH, doorW); + } else { + std::printf(" door : (none)\n"); + } + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + } // namespace bool handleGenMesh(int& i, int argc, char** argv, int& outRc) { @@ -7163,6 +7313,9 @@ bool handleGenMesh(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--gen-mesh-bookshelf") == 0 && i + 1 < argc) { outRc = handleBookshelf(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--gen-mesh-tent") == 0 && i + 1 < argc) { + outRc = handleTent(i, argc, argv); return true; + } if (std::strcmp(argv[i], "--gen-mesh-table") == 0 && i + 1 < argc) { outRc = handleTable(i, argc, argv); return true; } diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 25fe70c6..b6249570 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -200,6 +200,8 @@ void printUsage(const char* argv0) { std::printf(" Hexagonal coffin: narrow head + wide shoulder + tapered foot prism (default 2.0/0.8/0.6)\n"); std::printf(" --gen-mesh-bookshelf [width] [height] [depth] [shelves]\n"); std::printf(" Bookshelf: 5-panel cabinet with N-1 shelves and rows of varied book boxes (default 1.5/2.0/0.4/4)\n"); + std::printf(" --gen-mesh-tent [length] [width] [height] [doorH] [doorW]\n"); + std::printf(" Tent: A-frame canvas tent — ridge along X, two sloped roof panels, two gables, door notch on +X (default 1.6/1.0/0.9/0.5/0.4)\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");