From 9b24e0be8af350a3eb53eb3019a49ad128504ad2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 13:46:25 -0700 Subject: [PATCH] feat(editor): add --export-stl for 3D-printer-compatible WOM export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ASCII STL is the universal 3D printing format — Cura, PrusaSlicer, Bambu Studio, Slic3r, OctoPrint, MakerBot Print, and basically every slicer made in the last 25 years opens it natively. Lets WOM models drive physical prints with no conversion friction beyond one command: wowee_editor --export-stl Tree # -> Tree.stl wowee_editor --export-stl Tree out.stl Per-spec STL ASCII output: - 'solid ' header / 'endsolid ' footer (name sanitized to alphanum + underscore for slicers that strict-parse) - Per-triangle 'facet normal nx ny nz' with normal computed from cross-product of edges 1 and 2 (most slicers use this for orientation hints; falls back to (0,0,1) for degenerate triangles) - 'outer loop' with three vertex lines per facet - No shared vertex pool — STL stores every triangle independently Why STL alongside OBJ + glTF: OBJ targets DCC tools (Blender etc.), glTF targets web 3D viewers (Sketchfab, three.js), and STL targets fabrication. Three different ecosystems, three different format needs — wowee open formats now bridge to all three. Verified on a 5-vert/6-tri pyramid: STL has 6 facets with correctly computed normals (0 -1 0 for the bottom faces, computed slopes for the side triangles), proper solid/endsolid framing, name preserved ('solid Pyramid'). --- tools/editor/main.cpp | 74 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index a8c6fdd8..a8ed7e0c 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -474,6 +474,8 @@ static void printUsage(const char* argv0) { std::printf(" Convert a WOM model to Wavefront OBJ for use in Blender/MeshLab\n"); std::printf(" --export-glb [out.glb]\n"); std::printf(" Convert a WOM model to glTF 2.0 binary (.glb) — modern industry standard\n"); + std::printf(" --export-stl [out.stl]\n"); + std::printf(" Convert a WOM model to ASCII STL — works with any 3D printer slicer\n"); std::printf(" --export-wob-glb [out.glb]\n"); std::printf(" Convert a WOB building to glTF 2.0 binary (one mesh, per-group primitives)\n"); std::printf(" --export-whm-glb [out.glb]\n"); @@ -623,6 +625,7 @@ int main(int argc, char* argv[]) { "--export-wob-obj", "--import-wob-obj", "--export-woc-obj", "--export-whm-obj", "--export-glb", "--export-wob-glb", "--export-whm-glb", + "--export-stl", "--convert-m2", "--convert-wmo", "--convert-dbc-json", "--convert-json-dbc", "--convert-blp-png", "--migrate-wom", "--migrate-zone", @@ -4110,6 +4113,77 @@ int main(int argc, char* argv[]) { std::printf(" %u verts, %u tris, %zu primitive(s), %u-byte binary chunk\n", vCount, iCount / 3, primitives.size(), binLen); return 0; + } else if (std::strcmp(argv[i], "--export-stl") == 0 && i + 1 < argc) { + // ASCII STL export — single most universal 3D-printer format. + // Cura, PrusaSlicer, Bambu Studio, Slic3r, OctoPrint, MakerBot + // — every slicer made in the last 25 years opens STL natively. + // Lets WOM models drive physical prints with no conversion + // friction beyond this one command. + std::string base = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") + base = base.substr(0, base.size() - 4); + if (!wowee::pipeline::WoweeModelLoader::exists(base)) { + std::fprintf(stderr, "WOM not found: %s.wom\n", base.c_str()); + return 1; + } + if (outPath.empty()) outPath = base + ".stl"; + auto wom = wowee::pipeline::WoweeModelLoader::load(base); + if (!wom.isValid()) { + std::fprintf(stderr, "WOM has no geometry: %s.wom\n", base.c_str()); + return 1; + } + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, "Failed to open output: %s\n", outPath.c_str()); + return 1; + } + // STL solid name must be alphanumeric + underscores per loose + // convention; sanitize whatever the WOM name contains. Empty + // -> 'wowee_model'. + std::string solidName = wom.name.empty() ? "wowee_model" : wom.name; + for (auto& c : solidName) { + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_')) c = '_'; + } + out << "solid " << solidName << "\n"; + // Per-triangle facet — STL has no shared vertex pool, every + // triangle stands alone. Compute face normal from cross product + // (STL spec requires unit-length face normal; viewers fall + // back to per-vertex if zero, but most slicers want the real + // value for orientation hints). + uint32_t triCount = 0; + for (size_t k = 0; k + 2 < wom.indices.size(); k += 3) { + uint32_t i0 = wom.indices[k]; + uint32_t i1 = wom.indices[k + 1]; + uint32_t i2 = wom.indices[k + 2]; + if (i0 >= wom.vertices.size() || i1 >= wom.vertices.size() || + i2 >= wom.vertices.size()) continue; + const auto& v0 = wom.vertices[i0].position; + const auto& v1 = wom.vertices[i1].position; + const auto& v2 = wom.vertices[i2].position; + glm::vec3 e1 = v1 - v0; + glm::vec3 e2 = v2 - v0; + glm::vec3 n = glm::cross(e1, e2); + float len = glm::length(n); + if (len > 1e-12f) n /= len; + else n = {0, 0, 1}; // degenerate — STL spec allows any unit normal + out << " facet normal " << n.x << " " << n.y << " " << n.z << "\n" + << " outer loop\n" + << " vertex " << v0.x << " " << v0.y << " " << v0.z << "\n" + << " vertex " << v1.x << " " << v1.y << " " << v1.z << "\n" + << " vertex " << v2.x << " " << v2.y << " " << v2.z << "\n" + << " endloop\n" + << " endfacet\n"; + triCount++; + } + out << "endsolid " << solidName << "\n"; + out.close(); + std::printf("Exported %s.wom -> %s\n", base.c_str(), outPath.c_str()); + std::printf(" solid '%s', %u facets\n", + solidName.c_str(), triCount); + return 0; } else if (std::strcmp(argv[i], "--export-wob-glb") == 0 && i + 1 < argc) { // glTF 2.0 binary export for WOB. Same purpose as --export-glb // for WOM but adapted for buildings: each WOB group becomes