From f8337ee73f78c0da51e0b444541592219471fc10 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 05:57:33 -0700 Subject: [PATCH] feat(editor): add --scale-mesh and --translate-mesh basic transforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two in-place mesh transforms covering the most common authoring fix-ups: --scale-mesh Multiplies every vertex position, bone pivot, animation translation keyframe, and bounds (min/max/radius) by . Normals are unchanged (uniform scale preserves direction). Useful for "I imported this OBJ but it's the wrong size" fixes. Factor must be positive + finite. --translate-mesh Offsets vertices, bone pivots, and bounds by (dx, dy, dz). Animation keyframes are bone-local so they're left alone — only pivots shift. Radius stays constant (rigid translation). Verified: unit cube scale 3x → bounds ±1.5, radius 2.598; translate (10, 0, 0) → bounds (8.5,-1.5,-1.5)..(11.5,1.5,1.5); negative scale (-1) rejected with exit 1. Brings command count to 216. --- tools/editor/main.cpp | 121 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 8257188b..079a79c0 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -538,6 +538,10 @@ static void printUsage(const char* argv0) { std::printf(" Procedural straight staircase along +X with N steps (default 5 / 0.2 / 0.3 / 1.0)\n"); std::printf(" --add-texture-to-mesh [batchIdx]\n"); std::printf(" Bind an existing PNG into a WOM's texturePaths and point batchIdx (default 0) at it\n"); + std::printf(" --scale-mesh \n"); + std::printf(" Uniformly scale every vertex and bounds by (factor > 0)\n"); + std::printf(" --translate-mesh \n"); + std::printf(" Offset every vertex and bounds by (dx, dy, dz)\n"); std::printf(" --add-item [id] [quality] [displayId] [itemLevel]\n"); std::printf(" Append one item entry to /items.json (auto-creates the file)\n"); std::printf(" --list-items [--json]\n"); @@ -953,6 +957,7 @@ int main(int argc, char* argv[]) { "--export-data-tree-md", "--gen-texture", "--gen-mesh", "--gen-mesh-textured", "--add-texture-to-mesh", "--add-texture-to-zone", "--gen-mesh-stairs", "--gen-texture-gradient", + "--scale-mesh", "--translate-mesh", "--validate-glb", "--info-glb", "--info-glb-tree", "--info-glb-bytes", "--validate-jsondbc", "--check-glb-bounds", "--validate-stl", "--validate-png", "--validate-blp", @@ -16095,6 +16100,122 @@ int main(int argc, char* argv[]) { pngPath.c_str(), expected.c_str()); } return 0; + } else if (std::strcmp(argv[i], "--scale-mesh") == 0 && i + 2 < argc) { + // Uniformly scale a WOM in place. Multiplies every + // vertex position, every bone pivot, and the bounds by + // . Normals are unchanged (uniform scale + // preserves direction). Useful for "I imported this OBJ + // but it's the wrong size" cleanup. + std::string womBase = argv[++i]; + float factor = 1.0f; + try { factor = std::stof(argv[++i]); } + catch (...) { + std::fprintf(stderr, + "scale-mesh: must be a number\n"); + return 1; + } + if (factor <= 0.0f || !std::isfinite(factor)) { + std::fprintf(stderr, + "scale-mesh: factor must be positive and finite (got %g)\n", + factor); + return 1; + } + if (womBase.size() >= 4 && + womBase.substr(womBase.size() - 4) == ".wom") { + womBase = womBase.substr(0, womBase.size() - 4); + } + if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) { + std::fprintf(stderr, + "scale-mesh: %s.wom does not exist\n", womBase.c_str()); + return 1; + } + auto wom = wowee::pipeline::WoweeModelLoader::load(womBase); + if (!wom.isValid()) { + std::fprintf(stderr, + "scale-mesh: failed to load %s.wom\n", womBase.c_str()); + return 1; + } + for (auto& v : wom.vertices) v.position *= factor; + for (auto& b : wom.bones) b.pivot *= factor; + // Animation translations also scale; rotation/scale + // tracks are dimensionless. + for (auto& a : wom.animations) { + for (auto& bone : a.boneKeyframes) { + for (auto& kf : bone) kf.translation *= factor; + } + } + wom.boundMin *= factor; + wom.boundMax *= factor; + wom.boundRadius *= factor; + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "scale-mesh: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Scaled %s.wom by %g\n", womBase.c_str(), factor); + std::printf(" new bounds : (%.3f, %.3f, %.3f) - (%.3f, %.3f, %.3f)\n", + wom.boundMin.x, wom.boundMin.y, wom.boundMin.z, + wom.boundMax.x, wom.boundMax.y, wom.boundMax.z); + std::printf(" new radius : %.3f\n", wom.boundRadius); + return 0; + } else if (std::strcmp(argv[i], "--translate-mesh") == 0 && i + 4 < argc) { + // Offset every vertex (and bones / anim translations / + // bounds) by (dx, dy, dz). Useful for re-centering a + // mesh whose origin was wrong on import, or for shifting + // a procedural primitive that isn't centered the way + // you want. + std::string womBase = argv[++i]; + float dx = 0, dy = 0, dz = 0; + try { + dx = std::stof(argv[++i]); + dy = std::stof(argv[++i]); + dz = std::stof(argv[++i]); + } catch (...) { + std::fprintf(stderr, + "translate-mesh: dx/dy/dz must be numbers\n"); + return 1; + } + if (!std::isfinite(dx) || !std::isfinite(dy) || !std::isfinite(dz)) { + std::fprintf(stderr, + "translate-mesh: offsets must be finite\n"); + return 1; + } + if (womBase.size() >= 4 && + womBase.substr(womBase.size() - 4) == ".wom") { + womBase = womBase.substr(0, womBase.size() - 4); + } + if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) { + std::fprintf(stderr, + "translate-mesh: %s.wom does not exist\n", womBase.c_str()); + return 1; + } + auto wom = wowee::pipeline::WoweeModelLoader::load(womBase); + if (!wom.isValid()) { + std::fprintf(stderr, + "translate-mesh: failed to load %s.wom\n", womBase.c_str()); + return 1; + } + glm::vec3 d(dx, dy, dz); + for (auto& v : wom.vertices) v.position += d; + for (auto& b : wom.bones) b.pivot += d; + // Bone-relative animation translations don't shift with + // the model — only the bone pivots do, since translations + // are in bone-local space. Leave anim keyframes alone. + wom.boundMin += d; + wom.boundMax += d; + // Radius is unchanged (translation is rigid, doesn't + // change extent). + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "translate-mesh: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Translated %s.wom by (%g, %g, %g)\n", + womBase.c_str(), dx, dy, dz); + std::printf(" new bounds : (%.3f, %.3f, %.3f) - (%.3f, %.3f, %.3f)\n", + wom.boundMin.x, wom.boundMin.y, wom.boundMin.z, + wom.boundMax.x, wom.boundMax.y, wom.boundMax.z); + return 0; } else if (std::strcmp(argv[i], "--add-texture-to-zone") == 0 && i + 2 < argc) { // Import an existing PNG into a zone directory. Useful // for the "I have an artist-painted texture, get it into