From 010cf3b6c5a34f84fc7d7ce6437f4c6da6d83af1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 21:59:58 -0700 Subject: [PATCH] feat(editor): add --gen-mesh-fence repeating post + rail primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repeating fence: N square posts along +X spaced postSpacing apart, with two horizontal rails (top at 80% of post height, bottom at 30%) connecting consecutive posts. Posts are railThick × 2 wide; rails are thinner. Useful for fences around plots, pen boundaries, walkway dividers, garden beds, paddock perimeters. The 14th procedural primitive. Args: [posts] [postSpacing] [postHeight] [railThick] Defaults: 5 / 1.0 / 1.0 / 0.05. Posts hard-capped at 256. Verified: 5 posts × 1.0 spacing → 312 verts / 156 tris, X span of 4.0 (= 4 gaps × 1.0). --- tools/editor/main.cpp | 127 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 06d392dd..94bba475 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -563,6 +563,8 @@ static void printUsage(const char* argv0) { std::printf(" Doorway arch: two columns + semicircular top (default 1.0/1.5/0.2/0.3, 12 segs)\n"); std::printf(" --gen-mesh-pyramid [sides] [baseRadius] [height]\n"); std::printf(" N-sided polygonal pyramid with apex at +Y (default 4 sides, 1.0/1.0)\n"); + std::printf(" --gen-mesh-fence [posts] [postSpacing] [postHeight] [railThick]\n"); + std::printf(" Repeating fence: N posts along +X with two horizontal rails between\n"); std::printf(" --displace-mesh [scale]\n"); std::printf(" Offset each vertex along its normal by heightmap brightness × scale (default 1.0)\n"); std::printf(" --gen-mesh-from-heightmap [scaleXZ] [scaleY]\n"); @@ -1051,7 +1053,7 @@ int main(int argc, char* argv[]) { "--add-texture-to-mesh", "--add-texture-to-zone", "--gen-mesh-stairs", "--gen-mesh-grid", "--gen-mesh-disc", "--gen-mesh-tube", "--gen-mesh-capsule", "--gen-mesh-arch", - "--gen-mesh-pyramid", + "--gen-mesh-pyramid", "--gen-mesh-fence", "--gen-texture-gradient", "--gen-mesh-from-heightmap", "--export-mesh-heightmap", "--displace-mesh", @@ -19546,6 +19548,129 @@ int main(int argc, char* argv[]) { std::printf(" triangles : %zu (%d sides + %d base)\n", wom.indices.size() / 3, sides, sides); return 0; + } else if (std::strcmp(argv[i], "--gen-mesh-fence") == 0 && i + 1 < argc) { + // Repeating fence: N square posts along +X spaced + // apart, with two horizontal rails (top + // and bottom) connecting consecutive posts. Posts span + // from Y=0 up to Y=postHeight; each post is a small box + // of width = railThick × 2. + // + // Useful for fences around plots, pen boundaries, + // walkway dividers, garden beds. + std::string womBase = argv[++i]; + int posts = 5; + float spacing = 1.0f; + float postH = 1.0f; + float rt = 0.05f; // rail/post thickness + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { posts = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { spacing = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { postH = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { rt = std::stof(argv[++i]); } catch (...) {} + } + if (posts < 2 || posts > 256 || + spacing <= 0 || postH <= 0 || rt <= 0) { + std::fprintf(stderr, + "gen-mesh-fence: posts 2..256, spacing/height/thick > 0\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); + }; + auto addBox = [&](glm::vec3 lo, glm::vec3 hi) { + struct Face { float nx, ny, nz; float verts[4][3]; }; + Face faces[6] = { + { 0, 1, 0, {{lo.x,hi.y,hi.z},{hi.x,hi.y,hi.z},{hi.x,hi.y,lo.z},{lo.x,hi.y,lo.z}}}, + { 0, -1, 0, {{lo.x,lo.y,lo.z},{hi.x,lo.y,lo.z},{hi.x,lo.y,hi.z},{lo.x,lo.y,hi.z}}}, + { 0, 0, 1, {{lo.x,lo.y,hi.z},{hi.x,lo.y,hi.z},{hi.x,hi.y,hi.z},{lo.x,hi.y,hi.z}}}, + { 0, 0, -1, {{hi.x,lo.y,lo.z},{lo.x,lo.y,lo.z},{lo.x,hi.y,lo.z},{hi.x,hi.y,lo.z}}}, + { 1, 0, 0, {{hi.x,lo.y,hi.z},{hi.x,lo.y,lo.z},{hi.x,hi.y,lo.z},{hi.x,hi.y,hi.z}}}, + {-1, 0, 0, {{lo.x,lo.y,lo.z},{lo.x,lo.y,hi.z},{lo.x,hi.y,hi.z},{lo.x,hi.y,lo.z}}}, + }; + float uvs[4][2] = {{0,0},{1,0},{1,1},{0,1}}; + for (auto& f : faces) { + uint32_t base = static_cast(wom.vertices.size()); + for (int k = 0; k < 4; ++k) { + addV(glm::vec3(f.verts[k][0], f.verts[k][1], f.verts[k][2]), + glm::vec3(f.nx, f.ny, f.nz), + glm::vec2(uvs[k][0], uvs[k][1])); + } + wom.indices.push_back(base + 0); + wom.indices.push_back(base + 1); + wom.indices.push_back(base + 2); + wom.indices.push_back(base + 0); + wom.indices.push_back(base + 2); + wom.indices.push_back(base + 3); + } + }; + float postHalfW = rt; + // Posts along +X starting at X=0. + for (int k = 0; k < posts; ++k) { + float cx = k * spacing; + addBox(glm::vec3(cx - postHalfW, -postHalfW, 0), + glm::vec3(cx + postHalfW, postHalfW, postH)); + } + // Rails between consecutive posts. Two rails per gap: + // top (~80% up) and bottom (~30% up). + float topRailZ = postH * 0.8f; + float botRailZ = postH * 0.3f; + float railHalfH = rt * 0.5f; // rail is thinner than posts + for (int k = 0; k + 1 < posts; ++k) { + float xL = k * spacing + postHalfW; + float xR = (k + 1) * spacing - postHalfW; + if (xR <= xL) continue; // posts touching + addBox(glm::vec3(xL, -railHalfH, topRailZ - railHalfH), + glm::vec3(xR, railHalfH, topRailZ + railHalfH)); + addBox(glm::vec3(xL, -railHalfH, botRailZ - railHalfH), + glm::vec3(xR, railHalfH, botRailZ + railHalfH)); + } + // Bounds. + wom.boundMin = glm::vec3(-postHalfW, -postHalfW, 0); + wom.boundMax = glm::vec3((posts - 1) * spacing + postHalfW, + postHalfW, postH); + wom.boundRadius = glm::length(wom.boundMax - wom.boundMin) * 0.5f; + wowee::pipeline::WoweeModel::Batch b; + b.indexStart = 0; + b.indexCount = static_cast(wom.indices.size()); + b.textureIndex = 0; + b.blendMode = 0; + b.flags = 0; + wom.batches.push_back(b); + wom.texturePaths.push_back(""); + std::filesystem::path womPath(womBase); + std::filesystem::create_directories(womPath.parent_path()); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-fence: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" posts : %d\n", posts); + std::printf(" spacing : %.3f\n", spacing); + std::printf(" height : %.3f\n", postH); + std::printf(" thickness : %.3f\n", rt); + std::printf(" span X : %.3f\n", (posts - 1) * spacing); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; } else if (std::strcmp(argv[i], "--displace-mesh") == 0 && i + 2 < argc) { // Displaces each vertex along its current normal by the // heightmap brightness × scale. UVs determine where each