mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
feat(editor): add --audit-watertight-wob building QA tool
Sibling of --audit-watertight that walks every .wob under <root> and runs the welded watertight check on every group. A WOB passes only if every group is closed — interior rooms in a real building should each be a closed solid even though the building as a whole has intentional portal openings between them. Per-failure detail lists which groups failed and why (boundary edge count + non-manifold edge count). Exit code is the number of failed buildings (capped at 255) — same CI-friendly contract as the WOM audit. Smoke tested against /tmp/migtest cube.wob: PASS, 12 tris, exit code 0.
This commit is contained in:
parent
f1528f2dd7
commit
26f1947c84
3 changed files with 138 additions and 1 deletions
|
|
@ -2,6 +2,7 @@
|
|||
#include "cli_weld.hpp"
|
||||
|
||||
#include "pipeline/wowee_model.hpp"
|
||||
#include "pipeline/wowee_building.hpp"
|
||||
#include <glm/glm.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
|
|
@ -433,6 +434,136 @@ int handleAuditWatertight(int& i, int argc, char** argv) {
|
|||
return std::min(failCount, 255);
|
||||
}
|
||||
|
||||
// Welded watertight check on a single WOB. Each group is welded
|
||||
// independently (rooms separated by portals must stay distinct
|
||||
// collision surfaces). A WOB passes if EVERY group is closed —
|
||||
// per-group failure breakdowns are returned via the out vectors.
|
||||
bool isWobWatertightAfterWeld(
|
||||
const std::string& wobBase, float eps,
|
||||
std::vector<std::string>& outFailedGroups,
|
||||
std::size_t& outTotalTris,
|
||||
std::size_t& outTotalBoundary,
|
||||
std::size_t& outTotalNonManifold) {
|
||||
auto bld = wowee::pipeline::WoweeBuildingLoader::load(wobBase);
|
||||
outTotalTris = outTotalBoundary = outTotalNonManifold = 0;
|
||||
if (!bld.isValid()) return false;
|
||||
bool allOk = true;
|
||||
for (const auto& g : bld.groups) {
|
||||
if (g.indices.size() % 3 != 0) {
|
||||
outFailedGroups.push_back(g.name + " (indices%3 != 0)");
|
||||
allOk = false;
|
||||
continue;
|
||||
}
|
||||
outTotalTris += g.indices.size() / 3;
|
||||
std::vector<glm::vec3> positions;
|
||||
positions.reserve(g.vertices.size());
|
||||
for (const auto& v : g.vertices) positions.push_back(v.position);
|
||||
std::size_t uniq = 0;
|
||||
auto canon = buildWeldMap(positions, eps, uniq);
|
||||
EdgeStats edges = classifyEdges(g.indices, canon);
|
||||
outTotalBoundary += edges.boundary;
|
||||
outTotalNonManifold += edges.nonManifold;
|
||||
if (!edges.watertight()) {
|
||||
outFailedGroups.push_back(
|
||||
g.name + " (" + std::to_string(edges.boundary) +
|
||||
" boundary, " + std::to_string(edges.nonManifold) +
|
||||
" non-manifold)");
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
return allOk;
|
||||
}
|
||||
|
||||
int handleAuditWatertightWob(int& i, int argc, char** argv) {
|
||||
// Walk every .wob under <root> and run the welded-watertight
|
||||
// check on every group. PASS only if all groups in all WOBs
|
||||
// are closed — a real building's interior groups should each
|
||||
// be a closed surface even though the building as a whole has
|
||||
// intentional portal openings between them.
|
||||
std::string root = argv[++i];
|
||||
bool jsonOut = false;
|
||||
float weldEps = 1e-4f;
|
||||
while (i + 1 < argc && argv[i + 1][0] == '-') {
|
||||
if (std::strcmp(argv[i + 1], "--json") == 0) {
|
||||
jsonOut = true; ++i;
|
||||
} else if (std::strcmp(argv[i + 1], "--weld") == 0 && i + 2 < argc) {
|
||||
try { weldEps = std::stof(argv[i + 2]); } catch (...) {}
|
||||
i += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
namespace fs = std::filesystem;
|
||||
if (!fs::exists(root) || !fs::is_directory(root)) {
|
||||
std::fprintf(stderr,
|
||||
"audit-watertight-wob: %s is not a directory\n", root.c_str());
|
||||
return 1;
|
||||
}
|
||||
struct Result {
|
||||
std::string rel;
|
||||
std::size_t tris;
|
||||
std::size_t boundary;
|
||||
std::size_t nonManifold;
|
||||
std::vector<std::string> failedGroups;
|
||||
bool ok;
|
||||
};
|
||||
std::vector<Result> rows;
|
||||
std::error_code ec;
|
||||
for (const auto& e : fs::recursive_directory_iterator(root, ec)) {
|
||||
if (!e.is_regular_file()) continue;
|
||||
if (e.path().extension() != ".wob") continue;
|
||||
std::string base = e.path().string();
|
||||
base = base.substr(0, base.size() - 4);
|
||||
Result r;
|
||||
r.rel = fs::relative(e.path(), root).string();
|
||||
r.ok = isWobWatertightAfterWeld(base, weldEps, r.failedGroups,
|
||||
r.tris, r.boundary, r.nonManifold);
|
||||
rows.push_back(std::move(r));
|
||||
}
|
||||
std::sort(rows.begin(), rows.end(), [](const Result& a, const Result& b) {
|
||||
return a.rel < b.rel;
|
||||
});
|
||||
int failCount = 0;
|
||||
for (const auto& r : rows) if (!r.ok) ++failCount;
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["root"] = root;
|
||||
j["weldEps"] = weldEps;
|
||||
j["totalBuildings"] = rows.size();
|
||||
j["failures"] = failCount;
|
||||
nlohmann::json items = nlohmann::json::array();
|
||||
for (const auto& r : rows) {
|
||||
items.push_back({{"path", r.rel},
|
||||
{"triangles", r.tris},
|
||||
{"boundary", r.boundary},
|
||||
{"nonManifold", r.nonManifold},
|
||||
{"failedGroups", r.failedGroups},
|
||||
{"watertight", r.ok}});
|
||||
}
|
||||
j["buildings"] = items;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return std::min(failCount, 255);
|
||||
}
|
||||
std::printf("Watertight WOB audit: %s (weld eps %.6f)\n",
|
||||
root.c_str(), weldEps);
|
||||
if (rows.empty()) {
|
||||
std::printf(" No .wob files found.\n");
|
||||
return 0;
|
||||
}
|
||||
for (const auto& r : rows) {
|
||||
std::printf(" %s %s (%zu tris)\n",
|
||||
r.ok ? "PASS" : "FAIL", r.rel.c_str(), r.tris);
|
||||
if (!r.ok) {
|
||||
for (const auto& fg : r.failedGroups) {
|
||||
std::printf(" group %s\n", fg.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("\n TOTAL: %zu buildings, %d failure(s)\n",
|
||||
rows.size(), failCount);
|
||||
return std::min(failCount, 255);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleAudits(int& i, int argc, char** argv, int& outRc) {
|
||||
|
|
@ -463,6 +594,10 @@ bool handleAudits(int& i, int argc, char** argv, int& outRc) {
|
|||
outRc = handleAuditWatertight(i, argc, argv);
|
||||
return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--audit-watertight-wob") == 0 && i + 1 < argc) {
|
||||
outRc = handleAuditWatertightWob(i, argc, argv);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue