mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-08 01:53:52 +00:00
feat(editor): add --import-stl to round-trip STL back into WOM
Closes the WOM <-> STL bridge for the fabrication ecosystem (Cura, PrusaSlicer, TinkerCAD, Meshmixer, SolidWorks, Fusion 360). Designers can now edit prints in any CAD/3D-print tool and bring them back to the engine: asset_extract # M2 -> WOM (open binary) --export-stl # WOM -> STL (universal fabrication format) ... edit in TinkerCAD / sculpt in Meshmixer ... --import-stl # STL -> WOM (back to engine format) --validate-wom # confirm consistency Parser handles ASCII STL only (binary STL is a follow-up): - 'solid <name>' header — preserves model name - 'facet normal nx ny nz' — sets the face normal for following verts - 'vertex x y z' — accumulates 3 per facet - 'endfacet' — emits the triangle with the face normal applied to all 3 verts (STL doesn't carry per-vertex normals, so the round trip is faceted-shading by design) - Dedupes on (pos, normal) so shared vertices on the same face merge (a 4-vert square base with 2 tris collapses to 4 verts), but verts shared across faces with different normals stay distinct (correct for faceted geometry) - Computes bounds from positions for renderer culling Round-trip cost: a 5-vert/6-tri pyramid round-trips to 16 verts/6 tris. The vertex inflation is structural (STL faceted-shading semantics) — geometry preservation is exact. Verified via WOM -> STL -> WOM -> --validate-wom (PASSED, bounds and tri count match exactly).
This commit is contained in:
parent
9b24e0be8a
commit
a212479424
1 changed files with 125 additions and 1 deletions
|
|
@ -476,6 +476,8 @@ static void printUsage(const char* argv0) {
|
|||
std::printf(" Convert a WOM model to glTF 2.0 binary (.glb) — modern industry standard\n");
|
||||
std::printf(" --export-stl <wom-base> [out.stl]\n");
|
||||
std::printf(" Convert a WOM model to ASCII STL — works with any 3D printer slicer\n");
|
||||
std::printf(" --import-stl <stl-path> [wom-base]\n");
|
||||
std::printf(" Convert an ASCII STL back into WOM (round-trips with --export-stl)\n");
|
||||
std::printf(" --export-wob-glb <wob-base> [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 <wot-base> [out.glb]\n");
|
||||
|
|
@ -625,7 +627,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",
|
||||
"--export-stl", "--import-stl",
|
||||
"--convert-m2", "--convert-wmo",
|
||||
"--convert-dbc-json", "--convert-json-dbc", "--convert-blp-png",
|
||||
"--migrate-wom", "--migrate-zone",
|
||||
|
|
@ -4184,6 +4186,128 @@ int main(int argc, char* argv[]) {
|
|||
std::printf(" solid '%s', %u facets\n",
|
||||
solidName.c_str(), triCount);
|
||||
return 0;
|
||||
} else if (std::strcmp(argv[i], "--import-stl") == 0 && i + 1 < argc) {
|
||||
// ASCII STL -> WOM. Closes the STL round trip so designers can
|
||||
// edit prints in TinkerCAD/Meshmixer/SolidWorks and bring them
|
||||
// back to the engine. Dedupes vertices on (pos, normal) so the
|
||||
// resulting WOM vertex buffer stays compact.
|
||||
std::string stlPath = argv[++i];
|
||||
std::string womBase;
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') womBase = argv[++i];
|
||||
if (!std::filesystem::exists(stlPath)) {
|
||||
std::fprintf(stderr, "STL not found: %s\n", stlPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (womBase.empty()) {
|
||||
womBase = stlPath;
|
||||
if (womBase.size() >= 4 &&
|
||||
womBase.substr(womBase.size() - 4) == ".stl") {
|
||||
womBase = womBase.substr(0, womBase.size() - 4);
|
||||
}
|
||||
}
|
||||
std::ifstream in(stlPath);
|
||||
if (!in) {
|
||||
std::fprintf(stderr, "Failed to open STL: %s\n", stlPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
wowee::pipeline::WoweeModel wom;
|
||||
wom.version = 1;
|
||||
// Dedupe key: 6 floats (pos + normal) packed as a string. Loose
|
||||
// matching, but exact for round-trips since we write the same
|
||||
// floats back. Real-world STLs from CAD tools rarely benefit
|
||||
// from looser tolerance — they already share verts at the
|
||||
// exporter level.
|
||||
std::unordered_map<std::string, uint32_t> dedupe;
|
||||
auto interVert = [&](const glm::vec3& pos, const glm::vec3& nrm) {
|
||||
char key[128];
|
||||
std::snprintf(key, sizeof(key), "%.6f|%.6f|%.6f|%.6f|%.6f|%.6f",
|
||||
pos.x, pos.y, pos.z, nrm.x, nrm.y, nrm.z);
|
||||
auto it = dedupe.find(key);
|
||||
if (it != dedupe.end()) return it->second;
|
||||
wowee::pipeline::WoweeModel::Vertex v;
|
||||
v.position = pos;
|
||||
v.normal = nrm;
|
||||
v.texCoord = {0, 0};
|
||||
uint32_t idx = static_cast<uint32_t>(wom.vertices.size());
|
||||
wom.vertices.push_back(v);
|
||||
dedupe[key] = idx;
|
||||
return idx;
|
||||
};
|
||||
std::string line;
|
||||
std::string solidName;
|
||||
// Per-facet state: parsed normal + accumulating vertex queue.
|
||||
glm::vec3 currentNormal{0, 0, 1};
|
||||
std::vector<glm::vec3> facetVerts;
|
||||
int facetCount = 0;
|
||||
while (std::getline(in, line)) {
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
|
||||
line.pop_back();
|
||||
std::istringstream ss(line);
|
||||
std::string tok;
|
||||
ss >> tok;
|
||||
if (tok == "solid" && solidName.empty()) {
|
||||
ss >> solidName;
|
||||
} else if (tok == "facet") {
|
||||
std::string normalKw;
|
||||
ss >> normalKw;
|
||||
if (normalKw == "normal") {
|
||||
ss >> currentNormal.x >> currentNormal.y >> currentNormal.z;
|
||||
}
|
||||
facetVerts.clear();
|
||||
} else if (tok == "vertex") {
|
||||
glm::vec3 v;
|
||||
ss >> v.x >> v.y >> v.z;
|
||||
facetVerts.push_back(v);
|
||||
} else if (tok == "endfacet") {
|
||||
if (facetVerts.size() == 3) {
|
||||
// Use the facet normal for all 3 verts since STL
|
||||
// doesn't carry per-vertex normals. Glue-points to
|
||||
// adjacent facets will get distinct verts (which is
|
||||
// correct for faceted-shading STL geometry).
|
||||
for (const auto& v : facetVerts) {
|
||||
wom.indices.push_back(interVert(v, currentNormal));
|
||||
}
|
||||
facetCount++;
|
||||
}
|
||||
facetVerts.clear();
|
||||
}
|
||||
// 'outer loop', 'endloop', 'endsolid' ignored — we infer
|
||||
// from the vertex count per facet.
|
||||
}
|
||||
if (wom.vertices.empty() || wom.indices.empty()) {
|
||||
std::fprintf(stderr,
|
||||
"import-stl: no geometry parsed from %s\n", stlPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
wom.name = solidName.empty()
|
||||
? std::filesystem::path(stlPath).stem().string()
|
||||
: solidName;
|
||||
// Compute bounds — renderer culls by these so wrong values
|
||||
// make models disappear at distance.
|
||||
wom.boundMin = wom.vertices[0].position;
|
||||
wom.boundMax = wom.boundMin;
|
||||
for (const auto& v : wom.vertices) {
|
||||
wom.boundMin = glm::min(wom.boundMin, v.position);
|
||||
wom.boundMax = glm::max(wom.boundMax, v.position);
|
||||
}
|
||||
glm::vec3 center = (wom.boundMin + wom.boundMax) * 0.5f;
|
||||
float r2 = 0;
|
||||
for (const auto& v : wom.vertices) {
|
||||
glm::vec3 d = v.position - center;
|
||||
r2 = std::max(r2, glm::dot(d, d));
|
||||
}
|
||||
wom.boundRadius = std::sqrt(r2);
|
||||
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
|
||||
std::fprintf(stderr, "import-stl: failed to write %s.wom\n",
|
||||
womBase.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("Imported %s -> %s.wom\n", stlPath.c_str(), womBase.c_str());
|
||||
std::printf(" %d facets, %zu verts (deduped), bounds [%.2f, %.2f, %.2f] - [%.2f, %.2f, %.2f]\n",
|
||||
facetCount, wom.vertices.size(),
|
||||
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], "--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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue