mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
refactor(editor): extract edge classification into cli_weld
The "loop over triangles, key edges by canonical-vertex pair, count uses, classify boundary/manifold/non-manifold" pass was duplicated across cli_mesh_info, cli_world_info, and the new cli_audits watertight check. Hoist it into cli_weld as classifyEdges(indices, canon) returning an EdgeStats struct with boundary / manifold / nonManifold counters and a watertight() convenience method. All three callers verified byte-identical: • --info-mesh-stats firepit: 180 edges, watertight YES • --info-wob-stats cube: 18 manifold, watertight YES • --audit-watertight /tmp/...: 61 meshes, 12 failures, rc=12 About 60 more lines of duplication removed; classifyEdges + buildWeldMap together form the complete reusable surface for new weld/topology audit commands.
This commit is contained in:
parent
0137ca8707
commit
3cf3b35885
5 changed files with 90 additions and 91 deletions
|
|
@ -322,10 +322,9 @@ int handleValidateZonePack(int& i, int argc, char** argv) {
|
|||
}
|
||||
|
||||
// Watertight check on a single WOM after the standard weld pass.
|
||||
// Returns true if the welded mesh has zero boundary edges and zero
|
||||
// non-manifold edges. Stats are written through outBoundary /
|
||||
// outNonManifold for the per-zone audit's summary line. eps is the
|
||||
// caller-supplied weld tolerance.
|
||||
// Returns true if every welded edge is shared by exactly 2 triangles.
|
||||
// Per-mesh stats are written through outBoundary / outNonManifold
|
||||
// for the audit's summary line.
|
||||
bool isWomWatertightAfterWeld(
|
||||
const std::string& womBase, float eps,
|
||||
std::size_t& outTris, std::size_t& outBoundary,
|
||||
|
|
@ -341,32 +340,10 @@ bool isWomWatertightAfterWeld(
|
|||
for (const auto& v : wom.vertices) positions.push_back(v.position);
|
||||
std::size_t uniq = 0;
|
||||
auto canon = buildWeldMap(positions, eps, uniq);
|
||||
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);
|
||||
};
|
||||
std::unordered_map<uint64_t, uint32_t> edgeUses;
|
||||
edgeUses.reserve(outTris * 3);
|
||||
for (std::size_t t = 0; t < outTris; ++t) {
|
||||
uint32_t i0 = wom.indices[t * 3 + 0];
|
||||
uint32_t i1 = wom.indices[t * 3 + 1];
|
||||
uint32_t i2 = wom.indices[t * 3 + 2];
|
||||
if (i0 >= wom.vertices.size() || i1 >= wom.vertices.size() ||
|
||||
i2 >= wom.vertices.size()) {
|
||||
return false;
|
||||
}
|
||||
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)];
|
||||
}
|
||||
outBoundary = 0;
|
||||
outNonManifold = 0;
|
||||
for (const auto& [_k, count] : edgeUses) {
|
||||
if (count == 1) ++outBoundary;
|
||||
else if (count >= 3) ++outNonManifold;
|
||||
}
|
||||
return outBoundary == 0 && outNonManifold == 0;
|
||||
EdgeStats edges = classifyEdges(wom.indices, canon);
|
||||
outBoundary = edges.boundary;
|
||||
outNonManifold = edges.nonManifold;
|
||||
return edges.watertight();
|
||||
}
|
||||
|
||||
int handleAuditWatertight(int& i, int argc, char** argv) {
|
||||
|
|
|
|||
|
|
@ -395,17 +395,7 @@ int handleInfoMeshStats(int& i, int argc, char** argv) {
|
|||
}
|
||||
uniquePositions = wom.vertices.size();
|
||||
}
|
||||
// Edge-use counter: key is (lo<<32 | hi) of the two canonical
|
||||
// endpoint indices; value counts how many triangles share that
|
||||
// edge. Skipped for huge meshes (>2M tris) since the
|
||||
// unordered_map would balloon.
|
||||
const bool runEdgeAnalysis = (triCount <= 2'000'000);
|
||||
std::unordered_map<uint64_t, uint32_t> edgeUses;
|
||||
if (runEdgeAnalysis) edgeUses.reserve(triCount * 3);
|
||||
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);
|
||||
};
|
||||
// First pass: triangle areas + range checks (no edge work).
|
||||
for (std::size_t t = 0; t < triCount; ++t) {
|
||||
uint32_t i0 = wom.indices[t * 3 + 0];
|
||||
uint32_t i1 = wom.indices[t * 3 + 1];
|
||||
|
|
@ -420,21 +410,10 @@ int handleInfoMeshStats(int& i, int argc, char** argv) {
|
|||
glm::vec3 a = wom.vertices[i0].position;
|
||||
glm::vec3 b = wom.vertices[i1].position;
|
||||
glm::vec3 c = wom.vertices[i2].position;
|
||||
glm::vec3 e1 = b - a;
|
||||
glm::vec3 e2 = c - a;
|
||||
double area = 0.5 * glm::length(glm::cross(e1, e2));
|
||||
double area = 0.5 * glm::length(glm::cross(b - a, c - a));
|
||||
if (area < 1e-12) ++degenerate;
|
||||
areas.push_back(area);
|
||||
totalArea += area;
|
||||
if (runEdgeAnalysis) {
|
||||
uint32_t c0 = canon[i0], c1 = canon[i1], c2 = canon[i2];
|
||||
// Skip degenerate edges where the two endpoints map to
|
||||
// the same canonical vertex — they aren't real edges
|
||||
// after welding.
|
||||
if (c0 != c1) ++edgeUses[edgeKey(c0, c1)];
|
||||
if (c1 != c2) ++edgeUses[edgeKey(c1, c2)];
|
||||
if (c2 != c0) ++edgeUses[edgeKey(c2, c0)];
|
||||
}
|
||||
}
|
||||
double minArea = areas.empty() ? 0.0 :
|
||||
*std::min_element(areas.begin(), areas.end());
|
||||
|
|
@ -449,16 +428,15 @@ int handleInfoMeshStats(int& i, int argc, char** argv) {
|
|||
sortedAreas.end());
|
||||
medianArea = sortedAreas[sortedAreas.size() / 2];
|
||||
}
|
||||
std::size_t boundaryEdges = 0; // shared by 1 triangle
|
||||
std::size_t manifoldEdges = 0; // shared by 2
|
||||
std::size_t nonManifoldEdges = 0; // shared by 3+
|
||||
for (const auto& [_k, count] : edgeUses) {
|
||||
if (count == 1) ++boundaryEdges;
|
||||
else if (count == 2) ++manifoldEdges;
|
||||
else ++nonManifoldEdges;
|
||||
// Edge analysis via shared cli_weld utility. Skipped for huge
|
||||
// meshes (>2M tris) since the underlying unordered_map would
|
||||
// balloon.
|
||||
const bool runEdgeAnalysis = (triCount <= 2'000'000);
|
||||
EdgeStats edges;
|
||||
if (runEdgeAnalysis) {
|
||||
edges = classifyEdges(wom.indices, canon);
|
||||
}
|
||||
bool watertight = runEdgeAnalysis && boundaryEdges == 0 &&
|
||||
nonManifoldEdges == 0;
|
||||
bool watertight = runEdgeAnalysis && edges.watertight();
|
||||
glm::vec3 dim = wom.boundMax - wom.boundMin;
|
||||
double bboxVol = double(dim.x) * dim.y * dim.z;
|
||||
if (jsonOut) {
|
||||
|
|
@ -477,10 +455,10 @@ int handleInfoMeshStats(int& i, int argc, char** argv) {
|
|||
j["totalVertices"] = wom.vertices.size();
|
||||
}
|
||||
if (runEdgeAnalysis) {
|
||||
j["edges"] = {{"total", edgeUses.size()},
|
||||
{"boundary", boundaryEdges},
|
||||
{"manifold", manifoldEdges},
|
||||
{"nonManifold", nonManifoldEdges}};
|
||||
j["edges"] = {{"total", edges.total},
|
||||
{"boundary", edges.boundary},
|
||||
{"manifold", edges.manifold},
|
||||
{"nonManifold", edges.nonManifold}};
|
||||
j["watertight"] = watertight;
|
||||
}
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
|
|
@ -500,11 +478,11 @@ int handleInfoMeshStats(int& i, int argc, char** argv) {
|
|||
uniquePositions, wom.vertices.size(), weldEps);
|
||||
}
|
||||
if (runEdgeAnalysis) {
|
||||
std::printf(" edges : %zu total\n", edgeUses.size());
|
||||
std::printf(" boundary : %zu (open seams)\n", boundaryEdges);
|
||||
std::printf(" manifold : %zu (shared by 2 tris)\n", manifoldEdges);
|
||||
std::printf(" edges : %zu total\n", edges.total);
|
||||
std::printf(" boundary : %zu (open seams)\n", edges.boundary);
|
||||
std::printf(" manifold : %zu (shared by 2 tris)\n", edges.manifold);
|
||||
std::printf(" non-manifold : %zu (shared by 3+ tris)\n",
|
||||
nonManifoldEdges);
|
||||
edges.nonManifold);
|
||||
std::printf(" watertight : %s%s\n", watertight ? "YES" : "NO",
|
||||
useWeld ? " (after weld)" : "");
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <cmath>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
|
|
@ -34,6 +35,38 @@ std::vector<uint32_t> buildWeldMap(
|
|||
return canon;
|
||||
}
|
||||
|
||||
EdgeStats classifyEdges(const std::vector<uint32_t>& indices,
|
||||
const std::vector<uint32_t>& canon) {
|
||||
EdgeStats stats;
|
||||
if (indices.size() % 3 != 0) return stats;
|
||||
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);
|
||||
};
|
||||
std::unordered_map<uint64_t, uint32_t> edgeUses;
|
||||
edgeUses.reserve(indices.size());
|
||||
for (std::size_t t = 0; t + 2 < indices.size(); t += 3) {
|
||||
uint32_t i0 = indices[t + 0];
|
||||
uint32_t i1 = indices[t + 1];
|
||||
uint32_t i2 = indices[t + 2];
|
||||
if (i0 >= canon.size() || i1 >= canon.size() ||
|
||||
i2 >= canon.size()) {
|
||||
continue;
|
||||
}
|
||||
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)];
|
||||
}
|
||||
stats.total = edgeUses.size();
|
||||
for (const auto& [_k, count] : edgeUses) {
|
||||
if (count == 1) ++stats.boundary;
|
||||
else if (count == 2) ++stats.manifold;
|
||||
else ++stats.nonManifold;
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ namespace editor {
|
|||
namespace cli {
|
||||
|
||||
// Vertex weld pass shared by --info-mesh-stats / --info-wob-stats /
|
||||
// --bake-wom-collision. Positions are quantized onto a 1/eps grid;
|
||||
// every vertex sharing a cell with a previously-seen vertex is
|
||||
// remapped to that vertex's index. Returns canon[v] giving the
|
||||
// canonical (lowest-index) representative of v's equivalence class
|
||||
// and writes the count of distinct cells to `uniqueOut`.
|
||||
// --bake-wom-collision / --audit-watertight. Positions are quantized
|
||||
// onto a 1/eps grid; every vertex sharing a cell with a previously-
|
||||
// seen vertex is remapped to that vertex's index. Returns canon[v]
|
||||
// giving the canonical (lowest-index) representative of v's
|
||||
// equivalence class and writes the count of distinct cells to
|
||||
// `uniqueOut`.
|
||||
//
|
||||
// Implementation uses std::map<tuple<int64,int64,int64>, uint32_t>
|
||||
// for exact equality on the quantized key — a hash-based key would
|
||||
|
|
@ -25,6 +26,26 @@ std::vector<uint32_t> buildWeldMap(
|
|||
float eps,
|
||||
std::size_t& uniqueOut);
|
||||
|
||||
// Edge classification result from walking a triangle list with a
|
||||
// canon[] map (typically built by buildWeldMap above, but the
|
||||
// identity mapping also works for "as-authored" edge counts).
|
||||
struct EdgeStats {
|
||||
std::size_t total = 0; // distinct edges seen
|
||||
std::size_t boundary = 0; // shared by exactly 1 triangle (open seam)
|
||||
std::size_t manifold = 0; // shared by exactly 2 (closed surface)
|
||||
std::size_t nonManifold = 0; // shared by 3+ (branching surface)
|
||||
bool watertight() const {
|
||||
return boundary == 0 && nonManifold == 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Walk every triangle in `indices` (must be a multiple of 3),
|
||||
// remap each corner through canon[], and count edge uses. An
|
||||
// edge whose two canonical endpoints are equal is dropped (it
|
||||
// became a self-loop after welding and isn't a real edge).
|
||||
EdgeStats classifyEdges(const std::vector<uint32_t>& indices,
|
||||
const std::vector<uint32_t>& canon);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -95,10 +95,6 @@ int handleInfoWobStats(int& i, int argc, char** argv) {
|
|||
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;
|
||||
|
|
@ -140,8 +136,7 @@ int handleInfoWobStats(int& i, int argc, char** argv) {
|
|||
}
|
||||
gs.uniquePositions = g.vertices.size();
|
||||
}
|
||||
std::unordered_map<uint64_t, uint32_t> edgeUses;
|
||||
edgeUses.reserve(gs.tris * 3);
|
||||
// Triangle area pass (also catches out-of-range indices).
|
||||
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];
|
||||
|
|
@ -160,17 +155,12 @@ int handleInfoWobStats(int& i, int argc, char** argv) {
|
|||
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);
|
||||
EdgeStats edges = classifyEdges(g.indices, canon);
|
||||
gs.boundary = edges.boundary;
|
||||
gs.manifold = edges.manifold;
|
||||
gs.nonManifold = edges.nonManifold;
|
||||
gs.watertight = edges.watertight();
|
||||
aggBoundary += gs.boundary;
|
||||
aggManifold += gs.manifold;
|
||||
aggNonManifold += gs.nonManifold;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue