mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
feat(editor): --info-wob-stats + fix weld hash collision bug
Add --info-wob-stats reporting per-group + aggregate triangle counts, surface area, edge analysis, and watertight check for WOB buildings. Same flag surface as --info-mesh-stats including --weld <eps> for true topological closure check on per-face-vertex meshes. Also fixes a correctness bug in the weld implementation of --info-mesh-stats: the previous code used a 64-bit hash of the quantized position as the equality key, which gave false-positive collisions that incorrectly merged distinct vertices. A unit cube's 8 corners collapsed to 2 positions under the buggy hash. Replace with std::map keyed on the actual quantized (qx, qy, qz) tuple so equality is exact. Re-verified: cube 8→8 watertight YES; firepit 240→80 watertight YES (was wrongly reporting 56 unique with 48 non-manifold edges); tent_solid 18→6 watertight YES; tent_fixed 21→9 with 5 boundary edges at the door perimeter (correct — door is intentionally open).
This commit is contained in:
parent
a7989cc7ab
commit
4112b6d257
4 changed files with 208 additions and 12 deletions
|
|
@ -4,12 +4,19 @@
|
|||
#include "pipeline/wowee_collision.hpp"
|
||||
#include "pipeline/wowee_terrain_loader.hpp"
|
||||
#include "pipeline/adt_loader.hpp"
|
||||
#include <glm/glm.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
|
|
@ -61,6 +68,187 @@ int handleInfoWob(int& i, int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int handleInfoWobStats(int& i, int argc, char** argv) {
|
||||
// Geometric stats on a WOB building, per-group and aggregated
|
||||
// across all groups: triangle count, surface area, watertight
|
||||
// check via the same edge analysis as --info-mesh-stats. Pass
|
||||
// --weld <eps> to merge per-face vertex duplicates before edge
|
||||
// analysis (true topological closure check).
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = false;
|
||||
bool useWeld = false;
|
||||
float weldEps = 1e-5f;
|
||||
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) {
|
||||
useWeld = true;
|
||||
try { weldEps = std::stof(argv[i + 2]); } catch (...) {}
|
||||
i += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wob")
|
||||
base = base.substr(0, base.size() - 4);
|
||||
if (!wowee::pipeline::WoweeBuildingLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WOB not found: %s.wob\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto bld = wowee::pipeline::WoweeBuildingLoader::load(base);
|
||||
auto edgeKey = [](uint32_t a, uint32_t b) -> uint64_t {
|
||||
if (a > b) std::swap(a, b);
|
||||
return (uint64_t(a) << 32) | uint64_t(b);
|
||||
};
|
||||
struct GroupStats {
|
||||
std::string name;
|
||||
std::size_t tris = 0;
|
||||
std::size_t degenerate = 0;
|
||||
std::size_t uniquePositions = 0;
|
||||
std::size_t totalVerts = 0;
|
||||
std::size_t boundary = 0, manifold = 0, nonManifold = 0;
|
||||
bool watertight = false;
|
||||
double surfaceArea = 0.0;
|
||||
};
|
||||
std::vector<GroupStats> perGroup;
|
||||
perGroup.reserve(bld.groups.size());
|
||||
std::size_t aggBoundary = 0, aggManifold = 0, aggNonManifold = 0;
|
||||
std::size_t aggTris = 0, aggDegenerate = 0;
|
||||
double aggArea = 0.0;
|
||||
for (const auto& g : bld.groups) {
|
||||
GroupStats gs;
|
||||
gs.name = g.name;
|
||||
gs.totalVerts = g.vertices.size();
|
||||
if (g.indices.size() % 3 != 0) {
|
||||
std::fprintf(stderr,
|
||||
"info-wob-stats: group '%s' has indices %% 3 != 0\n",
|
||||
g.name.c_str());
|
||||
return 1;
|
||||
}
|
||||
gs.tris = g.indices.size() / 3;
|
||||
// Build canon[] for this group, optionally welding.
|
||||
std::vector<uint32_t> canon(g.vertices.size());
|
||||
if (useWeld) {
|
||||
// Tuple key (qx,qy,qz) gives exact equality matching;
|
||||
// a hash key would risk false-positive collisions
|
||||
// collapsing distinct corners. See cli_mesh_info.cpp
|
||||
// for the same pattern.
|
||||
const float invEps = 1.0f / std::max(weldEps, 1e-9f);
|
||||
using QKey = std::tuple<int64_t, int64_t, int64_t>;
|
||||
std::map<QKey, uint32_t> bucket;
|
||||
auto qkey = [&](const glm::vec3& p) -> QKey {
|
||||
return {static_cast<int64_t>(std::lround(p.x * invEps)),
|
||||
static_cast<int64_t>(std::lround(p.y * invEps)),
|
||||
static_cast<int64_t>(std::lround(p.z * invEps))};
|
||||
};
|
||||
for (std::size_t v = 0; v < g.vertices.size(); ++v) {
|
||||
QKey k = qkey(g.vertices[v].position);
|
||||
auto it = bucket.find(k);
|
||||
if (it == bucket.end()) {
|
||||
bucket.emplace(k, static_cast<uint32_t>(v));
|
||||
canon[v] = static_cast<uint32_t>(v);
|
||||
} else {
|
||||
canon[v] = it->second;
|
||||
}
|
||||
}
|
||||
gs.uniquePositions = bucket.size();
|
||||
} else {
|
||||
for (std::size_t v = 0; v < g.vertices.size(); ++v) {
|
||||
canon[v] = static_cast<uint32_t>(v);
|
||||
}
|
||||
gs.uniquePositions = g.vertices.size();
|
||||
}
|
||||
std::unordered_map<uint64_t, uint32_t> edgeUses;
|
||||
edgeUses.reserve(gs.tris * 3);
|
||||
for (std::size_t t = 0; t < gs.tris; ++t) {
|
||||
uint32_t i0 = g.indices[t * 3 + 0];
|
||||
uint32_t i1 = g.indices[t * 3 + 1];
|
||||
uint32_t i2 = g.indices[t * 3 + 2];
|
||||
if (i0 >= g.vertices.size() ||
|
||||
i1 >= g.vertices.size() ||
|
||||
i2 >= g.vertices.size()) {
|
||||
std::fprintf(stderr,
|
||||
"info-wob-stats: group '%s' has out-of-range index\n",
|
||||
g.name.c_str());
|
||||
return 1;
|
||||
}
|
||||
glm::vec3 a = g.vertices[i0].position;
|
||||
glm::vec3 b = g.vertices[i1].position;
|
||||
glm::vec3 c = g.vertices[i2].position;
|
||||
double area = 0.5 * glm::length(glm::cross(b - a, c - a));
|
||||
if (area < 1e-12) ++gs.degenerate;
|
||||
gs.surfaceArea += area;
|
||||
uint32_t c0 = canon[i0], c1 = canon[i1], c2 = canon[i2];
|
||||
if (c0 != c1) ++edgeUses[edgeKey(c0, c1)];
|
||||
if (c1 != c2) ++edgeUses[edgeKey(c1, c2)];
|
||||
if (c2 != c0) ++edgeUses[edgeKey(c2, c0)];
|
||||
}
|
||||
for (const auto& [_k, count] : edgeUses) {
|
||||
if (count == 1) ++gs.boundary;
|
||||
else if (count == 2) ++gs.manifold;
|
||||
else ++gs.nonManifold;
|
||||
}
|
||||
gs.watertight = (gs.boundary == 0 && gs.nonManifold == 0);
|
||||
aggBoundary += gs.boundary;
|
||||
aggManifold += gs.manifold;
|
||||
aggNonManifold += gs.nonManifold;
|
||||
aggTris += gs.tris;
|
||||
aggDegenerate += gs.degenerate;
|
||||
aggArea += gs.surfaceArea;
|
||||
perGroup.push_back(std::move(gs));
|
||||
}
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wob"] = base + ".wob";
|
||||
j["welded"] = useWeld;
|
||||
if (useWeld) j["weldEps"] = weldEps;
|
||||
j["aggregate"] = {{"groups", perGroup.size()},
|
||||
{"triangles", aggTris},
|
||||
{"degenerateTriangles", aggDegenerate},
|
||||
{"surfaceArea", aggArea},
|
||||
{"boundary", aggBoundary},
|
||||
{"manifold", aggManifold},
|
||||
{"nonManifold", aggNonManifold}};
|
||||
nlohmann::json gs = nlohmann::json::array();
|
||||
for (const auto& g : perGroup) {
|
||||
gs.push_back({{"name", g.name},
|
||||
{"triangles", g.tris},
|
||||
{"degenerate", g.degenerate},
|
||||
{"surfaceArea", g.surfaceArea},
|
||||
{"uniquePositions", g.uniquePositions},
|
||||
{"totalVerts", g.totalVerts},
|
||||
{"boundary", g.boundary},
|
||||
{"manifold", g.manifold},
|
||||
{"nonManifold", g.nonManifold},
|
||||
{"watertight", g.watertight}});
|
||||
}
|
||||
j["groups"] = gs;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WOB stats: %s.wob\n", base.c_str());
|
||||
std::printf(" groups : %zu\n", perGroup.size());
|
||||
std::printf(" total tris : %zu (%zu degenerate)\n",
|
||||
aggTris, aggDegenerate);
|
||||
std::printf(" total area : %.4f\n", aggArea);
|
||||
std::printf(" aggregate edges : %zu boundary, %zu manifold, %zu non-manifold\n",
|
||||
aggBoundary, aggManifold, aggNonManifold);
|
||||
if (useWeld) {
|
||||
std::printf(" weld eps : %.6f\n", weldEps);
|
||||
}
|
||||
std::printf("\n Per group:\n");
|
||||
std::printf(" idx tris area verts→uniq boundary manifold non-m closed\n");
|
||||
for (std::size_t k = 0; k < perGroup.size(); ++k) {
|
||||
const auto& g = perGroup[k];
|
||||
std::printf(" %3zu %5zu %8.3f %5zu→%-5zu %8zu %8zu %5zu %s\n",
|
||||
k, g.tris, g.surfaceArea,
|
||||
g.totalVerts, g.uniquePositions,
|
||||
g.boundary, g.manifold, g.nonManifold,
|
||||
g.watertight ? "YES" : "no");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleInfoWot(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = (i + 1 < argc &&
|
||||
|
|
@ -173,6 +361,9 @@ bool handleWorldInfo(int& i, int argc, char** argv, int& outRc) {
|
|||
if (std::strcmp(argv[i], "--info-wob") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfoWob(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wob-stats") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfoWobStats(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wot") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfoWot(i, argc, argv); return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue