ericw-tools/qbsp/tjunc.cc

900 lines
28 KiB
C++

/*
Copyright (C) 1996-1997 Id Software, Inc.
Copyright (C) 1997 Greg Lewis
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
See file, 'COPYING', for details.
*/
// tjunc.c
#include <qbsp/qbsp.hh>
#include <qbsp/map.hh>
#include <atomic>
struct tjunc_stats_t
{
// # of degenerate edges reported (with two identical input vertices)
std::atomic<size_t> degenerate;
// # of new edges created to close a tjunction
// (also technically the # of points detected that lay on other faces' edges)
std::atomic<size_t> tjunctions;
// # of faces that were created as a result of splitting faces that are too large
// to be contained on a single face
std::atomic<size_t> faceoverflows;
// # of faces that were degenerate and were just collapsed altogether.
std::atomic<size_t> facecollapse;
// # of faces that were able to be fixed just by rotating the start point.
std::atomic<size_t> rotates;
// # of faces that weren't able to be fixed with start point rotation
std::atomic<size_t> norotates;
// # of faces that could be successfully retopologized
std::atomic<size_t> retopology;
// # of faces generated by retopologization
std::atomic<size_t> faceretopology;
// # of faces that were successfully topologized by MWT
std::atomic<size_t> mwt;
// # of triangles computed by MWT
std::atomic<size_t> trimwt;
// # of faces added by MWT
std::atomic<size_t> facemwt;
};
inline std::optional<vec_t> PointOnEdge(
const qvec3d &p, const qvec3d &edge_start, const qvec3d &edge_dir, float start = 0, float end = 1)
{
qvec3d delta = p - edge_start;
vec_t dist = qv::dot(delta, edge_dir);
// check if off an end
if (dist <= start || dist >= end) {
return std::nullopt;
}
qvec3d exact = edge_start + (edge_dir * dist);
qvec3d off = p - exact;
vec_t error = qv::length(off);
// brushbsp-fixme: this was 0.5 in Q2, check?
if (fabs(error) > DEFAULT_ON_EPSILON) {
// not on the edge
return std::nullopt;
}
return dist;
}
/*
==========
TestEdge
Can be recursively reentered
==========
*/
inline void TestEdge(vec_t start, vec_t end, size_t p1, size_t p2, size_t startvert,
const std::vector<size_t> &edge_verts, const qvec3d &edge_start, const qvec3d &edge_dir,
std::vector<size_t> &superface, tjunc_stats_t &stats)
{
if (p1 == p2) {
// degenerate edge
stats.degenerate++;
return;
}
for (size_t k = startvert; k < edge_verts.size(); k++) {
size_t j = edge_verts[k];
if (j == p1 || j == p2) {
continue;
}
auto dist = PointOnEdge(map.bsp.dvertexes[j], edge_start, edge_dir, start, end);
if (!dist.has_value()) {
continue;
}
// break the edge
stats.tjunctions++;
TestEdge(start, dist.value(), p1, j, k + 1, edge_verts, edge_start, edge_dir, superface, stats);
TestEdge(dist.value(), end, j, p2, k + 1, edge_verts, edge_start, edge_dir, superface, stats);
return;
}
// the edge p1 to p2 is now free of tjunctions
superface.push_back(p1);
}
/*
==========
FindEdgeVerts_BruteForce
Force a dumb check of everything
==========
*/
static void FindEdgeVerts_BruteForce(
const node_t *, const node_t *, const qvec3d &, const qvec3d &, std::vector<size_t> &verts)
{
verts.resize(map.bsp.dvertexes.size());
for (size_t i = 0; i < verts.size(); i++) {
verts[i] = i;
}
}
/*
==========
FindEdgeVerts_FaceBounds_R
Recursive function for matching nodes that intersect the aabb
for vertex checking.
==========
*/
static void FindEdgeVerts_FaceBounds_R(const node_t *node, const aabb3d &aabb, std::vector<size_t> &verts)
{
if (node->is_leaf) {
return;
} else if (node->bounds.disjoint(aabb, 0.0)) {
return;
}
for (auto &face : node->facelist) {
for (auto &v : face->original_vertices) {
if (aabb.containsPoint(map.bsp.dvertexes[v])) {
verts.push_back(v);
}
}
}
FindEdgeVerts_FaceBounds_R(node->children[0].get(), aabb, verts);
FindEdgeVerts_FaceBounds_R(node->children[1].get(), aabb, verts);
}
/*
==========
FindEdgeVerts_FaceBounds
Use a loose AABB around the line and only capture vertices that intersect it.
==========
*/
static void FindEdgeVerts_FaceBounds(
const node_t *headnode, const qvec3d &p1, const qvec3d &p2, std::vector<size_t> &verts)
{
// magic number, average of "usual" points per edge
verts.reserve(8);
FindEdgeVerts_FaceBounds_R(headnode, (aabb3d{} + p1 + p2).grow(qvec3d(1.0, 1.0, 1.0)), verts);
}
/*
==================
SplitFaceIntoFragments
The face was created successfully, but may have way too many edges.
Cut it down to the minimum amount of faces that are within the
max edge count.
Modifies `superface`. Adds the results to the end of `output`.
==================
*/
inline void SplitFaceIntoFragments(
std::vector<size_t> &superface, std::list<std::vector<size_t>> &output, tjunc_stats_t &stats)
{
const int32_t &maxedges = qbsp_options.maxedges.value();
// split into multiple fragments, because of vertex overload
while (superface.size() > maxedges) {
stats.faceoverflows++;
// copy MAXEDGES from our current face
std::vector<size_t> &newf = output.emplace_back(maxedges);
std::copy_n(superface.begin(), maxedges, newf.begin());
// remove everything in-between from the superface
// except for the last edge we just wrote (0 and MAXEDGES-1)
std::copy(superface.begin() + maxedges - 1, superface.end(), superface.begin() + 1);
// resize superface; we need enough room to store the two extra verts
superface.resize(superface.size() - maxedges + 2);
}
// move the first face to the end, since that's logically where it belongs now
output.splice(output.end(), output, output.begin());
}
float AngleOfTriangle(const qvec3d &a, const qvec3d &b, const qvec3d &c)
{
vec_t num = (b[0] - a[0]) * (c[0] - a[0]) + (b[1] - a[1]) * (c[1] - a[1]) + (b[2] - a[2]) * (c[2] - a[2]);
vec_t den = sqrt(pow((b[0] - a[0]), 2) + pow((b[1] - a[1]), 2) + pow((b[2] - a[2]), 2)) *
sqrt(pow((c[0] - a[0]), 2) + pow((c[1] - a[1]), 2) + pow((c[2] - a[2]), 2));
return acos(num / den) * (180.0 / 3.141592653589793238463);
}
// Check whether the given input triangle would be valid
// on the given face and not have any other points
// intersecting it.
inline bool TriangleIsValid(size_t v0, size_t v1, size_t v2, vec_t angle_epsilon)
{
if (AngleOfTriangle(map.bsp.dvertexes[v0], map.bsp.dvertexes[v1], map.bsp.dvertexes[v2]) < angle_epsilon ||
AngleOfTriangle(map.bsp.dvertexes[v1], map.bsp.dvertexes[v2], map.bsp.dvertexes[v0]) < angle_epsilon ||
AngleOfTriangle(map.bsp.dvertexes[v2], map.bsp.dvertexes[v0], map.bsp.dvertexes[v1]) < angle_epsilon) {
return false;
}
return true;
}
/*
==================
CreateSuperFace
Generate a superface (the input face `f` but with all of the
verts in the world added that lay on the line) and return it
==================
*/
static std::vector<size_t> CreateSuperFace(node_t *headnode, face_t *f, tjunc_stats_t &stats)
{
std::vector<size_t> superface;
superface.reserve(f->original_vertices.size() * 2);
// stores all of the verts in the world that are close to
// being on a given edge
std::vector<size_t> edge_verts;
// find all of the extra vertices that lay on edges,
// place them in superface
for (size_t i = 0; i < f->original_vertices.size(); i++) {
auto v1 = f->original_vertices[i];
auto v2 = f->original_vertices[(i + 1) % f->original_vertices.size()];
qvec3d edge_start = map.bsp.dvertexes[v1];
qvec3d e2 = map.bsp.dvertexes[v2];
edge_verts.clear();
FindEdgeVerts_FaceBounds(headnode, edge_start, e2, edge_verts);
vec_t len;
qvec3d edge_dir = qv::normalize(e2 - edge_start, len);
TestEdge(0, len, v1, v2, 0, edge_verts, edge_start, edge_dir, superface, stats);
}
return superface;
}
#include <common/bsputils.hh>
#include <fstream>
using qvectri = qvec<size_t, 3>;
// check if the given triangle exists in the set of triangles
// in any permutation
std::optional<size_t> triangle_exists(const std::vector<qvectri> &triangles, size_t a, size_t b, size_t c)
{
for (size_t i = 0; i < triangles.size(); i++) {
auto &tri = triangles[i];
for (size_t s = 0; s < 3; s++) {
if (tri[s] == a && tri[(s + 1) % 3] == b && tri[(s + 2) % 3] == c) {
return i;
}
}
}
return std::nullopt;
}
// find the triangles best suited to create a
// fan out of in the given set of triangles.
std::vector<size_t> find_best_fan(const std::vector<qvectri> &triangles, size_t num_vertices)
{
// find the triangle with the most fannable vertices.
std::vector<size_t> best_triangles;
for (auto &tri : triangles) {
// try all three permutations
for (size_t perm = 0; perm < 3; perm++) {
size_t first = tri[perm];
size_t mid = tri[(perm + 1) % 3];
size_t last = tri[(perm + 2) % 3];
std::vector<size_t> my_tri;
// find any other that can be wound from this edge
// TODO: can optimize by only looping around the verts
// included in the triangle
for (; last != first; last = (last + 1) % num_vertices) {
auto ftri = triangle_exists(triangles, first, mid, last);
// no triangle found for A B C, so try again
// with A B D, etc.
if (ftri == std::nullopt) {
continue;
}
// found A B C, so go next (A C D)
my_tri.push_back(ftri.value());
mid = last;
}
if (best_triangles.empty() || my_tri.size() > best_triangles.size()) {
best_triangles = std::move(my_tri);
}
}
}
return best_triangles;
}
// find the seed vertex (vertex referenced by the most edges) of
// the fan.
size_t find_seed_vertex(const std::vector<qvectri> &triangles, const std::vector<size_t> &fan)
{
std::unordered_set<size_t> verts{triangles[fan[0]].begin(), triangles[fan[0]].end()};
for (size_t i = 1; i < fan.size(); i++) {
auto &tri = triangles[fan[i]];
// produce intersection
for (auto it = verts.begin(); it != verts.end();) {
if (std::find(tri.begin(), tri.end(), *it) == tri.end()) {
it = verts.erase(it);
} else {
it++;
}
}
// if there's only one vert left it has to be that one
if (verts.size() == 1) {
return *verts.begin();
}
}
// just pick whatever's left
return *verts.begin();
}
static std::list<std::vector<size_t>> compress_triangles_into_fans(
std::vector<qvectri> &triangles, const std::vector<size_t> &vertices)
{
std::list<std::vector<size_t>> tris_compiled;
while (triangles.size()) {
auto fan = find_best_fan(triangles, vertices.size());
Q_assert(fan.size());
// when we run into only 1 triangle fans left,
// just add the rest directly.
if (fan.size() == 1) {
for (auto &tri : triangles) {
tris_compiled.emplace_back(std::vector<size_t>{vertices[tri[0]], vertices[tri[1]], vertices[tri[2]]});
}
triangles.clear();
break;
}
// a fan can be made! find the seed vertex
auto seed = find_seed_vertex(triangles, fan);
struct tri_verts_less_pred
{
size_t seed, vert_count;
bool operator()(const size_t &a, const size_t &b) const
{
size_t ka = a < seed ? vert_count + a : a;
size_t kb = b < seed ? vert_count + b : b;
return ka < kb;
}
};
// add all the verts and order them so they match
// the proper winding
std::set<size_t, tri_verts_less_pred> verts(tri_verts_less_pred{seed, vertices.size()});
for (auto tri_index : fan) {
auto &tri = triangles[tri_index];
for (auto &v : tri) {
verts.insert(v);
}
}
Q_assert(verts.size() >= 3);
// add the new winding
auto &out_tri = tris_compiled.emplace_back(verts.begin(), verts.end());
for (auto &v : out_tri) {
v = vertices[v];
}
// remove all of the fans from the triangle list
std::sort(fan.begin(), fan.end(), [](auto &l, auto &r) { return l > r; });
for (auto tri_index : fan) {
triangles.erase(triangles.begin() + tri_index);
}
}
return tris_compiled;
}
#include <queue>
// Function to calculate the weight of optimal triangulation of a convex polygon
// represented by a given set of vertices
std::vector<qvectri> minimum_weight_triangulation(
const std::vector<size_t> &indices, const std::vector<qvec2d> &vertices)
{
// get the number of vertices in the polygon
size_t n = vertices.size();
// create a table for storing the solutions to subproblems
// `T[i][j]` stores the weight of the minimum-weight triangulation
// of the polygon below edge `ij`
std::vector<vec_t> T(n * n);
std::vector<std::optional<size_t>> K(n * n);
// fill the table diagonally using the recurrence relation
for (size_t diagonal = 0; diagonal < n; diagonal++) {
for (size_t i = 0, j = diagonal; j < n; i++, j++) {
// If the polygon has less than 3 vertices, triangulation is not possible
if (j < i + 2) {
continue;
}
T[i + (j * n)] = std::numeric_limits<vec_t>::max();
// consider all possible triangles `ikj` within the polygon
for (size_t k = i + 1; k <= j - 1; k++) {
// The weight of triangulation is the length of its perimeter
vec_t weight;
if (!TriangleIsValid(indices[i], indices[j], indices[k], 0.01)) {
weight = std::nexttoward(std::numeric_limits<vec_t>::max(), 0.0);
} else {
weight = (qv::distance(vertices[i], vertices[j]) + qv::distance(vertices[j], vertices[k]) +
qv::distance(vertices[k], vertices[i])) +
T[i + (k * n)] + T[k + (j * n)];
}
vec_t &t_weight = T[i + (j * n)];
// choose vertex `k` that leads to the minimum total weight
if (weight < t_weight) {
t_weight = weight;
K[i + (j * n)] = k;
}
}
}
}
std::vector<qvectri> triangles;
std::queue<qvec<size_t, 2>> edge_queue;
edge_queue.emplace(0, n - 1);
while (!edge_queue.empty()) {
auto edge = edge_queue.front();
edge_queue.pop();
if (edge[0] == edge[1]) {
continue;
}
auto &c = K[edge[0] + (edge[1] * n)];
if (!c.has_value()) {
continue;
}
qvectri tri{edge[0], edge[1], c.value()};
std::sort(tri.begin(), tri.end());
triangles.emplace_back(tri);
edge_queue.emplace(edge[0], c.value());
edge_queue.emplace(c.value(), edge[1]);
}
Q_assert(triangles.size() == n - 2);
return triangles;
}
static std::list<std::vector<size_t>> mwt_face(
const face_t *f, const std::vector<size_t> &vertices, tjunc_stats_t &stats)
{
auto p = f->plane;
if (f->plane_flipped) {
p = -p;
}
auto [u, v] = qv::MakeTangentAndBitangentUnnormalized(p.normal);
qv::normalizeInPlace(u);
qv::normalizeInPlace(v);
std::vector<qvec2d> points_2d(vertices.size());
for (size_t i = 0; i < vertices.size(); i++) {
points_2d[i] = {qv::dot(map.bsp.dvertexes[vertices[i]], u), qv::dot(map.bsp.dvertexes[vertices[i]], v)};
}
auto tris = minimum_weight_triangulation(vertices, points_2d);
stats.trimwt += tris.size();
return compress_triangles_into_fans(tris, vertices);
}
/*
==================
RetopologizeFace
A face has T-junctions that can't be resolved from rotation.
It's still a convex face with wound vertices, though, so we
can split it into several triangle fans.
==================
*/
static std::list<std::vector<size_t>> RetopologizeFace(const face_t *f, const std::vector<size_t> &vertices)
{
std::list<std::vector<size_t>> result;
// the copy we're working on
std::vector<size_t> input(vertices);
while (input.size()) {
// failure if we somehow degenerated a triangle
if (input.size() < 3) {
return {};
}
size_t seed = 0;
int64_t end = 0;
// find seed triangle (allowing a wrap around,
// because it's possible for only the last two triangles
// to be valid).
for (; seed < input.size(); seed++) {
auto v0 = input[seed];
auto v1 = input[(seed + 1) % input.size()];
end = (seed + 2) % input.size();
auto v2 = input[end];
if (!TriangleIsValid(v0, v1, v2, 0.01)) {
continue;
}
// if the next point lays on the edge of v0-v2, this next
// triangle won't be valid.
float len;
qvec3d dir = qv::normalize(map.bsp.dvertexes[v0] - map.bsp.dvertexes[v2], len);
auto dist =
PointOnEdge(map.bsp.dvertexes[input[(end + 1) % input.size()]], map.bsp.dvertexes[v2], dir, 0, len);
if (dist.has_value()) {
continue;
}
// good one!
break;
}
if (seed == input.size()) {
// can't find a non-zero area triangle; failure
return {};
}
// from the seed vertex, keep winding until we hit a zero-area triangle.
// we know that triangle (seed, end - 1, end) is valid, so we wind from
// end + 1 until we fully wrap around. We also can't include a triangle
// that has a point after it laying on the final edge.
size_t wrap = end;
end = (end + 1) % input.size();
for (; end != wrap; end = (end + 1) % input.size()) {
auto v0 = input[seed];
auto v1 = input[(end - 1) < 0 ? (input.size() - 1) : (end - 1)];
auto v2 = input[end];
// if the next point lays on the edge of v0-v2, this next
// triangle won't be valid.
float len;
qvec3d dir = qv::normalize(map.bsp.dvertexes[v0] - map.bsp.dvertexes[v2], len);
auto dist =
PointOnEdge(map.bsp.dvertexes[input[(end + 1) % input.size()]], map.bsp.dvertexes[v2], dir, 0, len);
if (dist.has_value()) {
// the next point lays on this edge, so back up and stop
end = (end - 1) < 0 ? input.size() - 1 : (end - 1);
break;
}
}
// now we have a fan from seed to end that is valid.
// add it to the result, clip off all of the
// points between it and restart the algorithm
// using that edge.
auto &tri = result.emplace_back();
// the whole fan can just be moved; we're finished.
if (seed == end) {
tri = std::move(input);
break;
} else if (end == wrap) {
// we successfully wrapped around, but the
// seed vertex isn't at the start, so rotate it.
// copy base -> end
tri.resize(input.size());
auto out = std::copy(input.begin() + seed, input.end(), tri.begin());
// copy end -> base
std::copy(input.begin(), input.begin() + seed, out);
break;
}
if (end < seed) {
// the end point is 'behind' the seed, so we're clipping
// off two sides of the input
size_t x = seed;
bool first = true;
while (true) {
if (x == end && !first) {
break;
}
tri.emplace_back(input[x]);
x = (x + 1) % input.size();
first = false;
}
} else {
// simple case where the end point is ahead of the seed;
// copy the range over to the output
std::copy(input.begin() + seed, input.begin() + end + 1, std::back_inserter(tri));
}
Q_assert(seed != end);
if (end < seed) {
// slightly more complex case: the end point is behind the seed point.
// 0 end 2 3 seed 5 6
// end 2 3 seed
// calculate new size
size_t new_size = (seed + 1) - end;
// move back the end to the start
std::copy(input.begin() + end, input.begin() + seed + 1, input.begin());
// clip
input.resize(new_size);
} else {
// simple case: the end point is ahead of the seed point.
// collapse the range after it backwards over top of the seed
// and clip it off
// 0 1 2 seed 4 5 6 end 8 9
// 0 1 2 seed end 8 9
// calculate new size
size_t new_size = input.size() - (end - seed - 1);
// move range
std::copy(input.begin() + end, input.end(), input.begin() + seed + 1);
// clip
input.resize(new_size);
}
}
// finished
return result;
}
/*
==================
FixFaceEdges
If the face has any T-junctions, fix them here.
==================
*/
static void FixFaceEdges(node_t *headnode, face_t *f, tjunc_stats_t &stats)
{
// we were asked not to bother fixing any of the faces.
if (qbsp_options.tjunc.value() == settings::tjunclevel_t::NONE) {
f->fragments.emplace_back(face_fragment_t{f->original_vertices});
return;
}
std::vector<size_t> superface = CreateSuperFace(headnode, f, stats);
if (superface.size() < 3) {
// entire face collapsed
stats.facecollapse++;
return;
} else if (superface.size() == 3) {
// no need to adjust this either
f->fragments.emplace_back(face_fragment_t{f->original_vertices});
return;
}
// faces with 4 or more vertices can be done better.
// temporary storage for result faces; stored as a list
// since a resize may steal references out from underneath us
// as the functions do their work.
std::list<std::vector<size_t>> faces;
// do MWT first; it will generate optimal results for everything.
if (qbsp_options.tjunc.value() >= settings::tjunclevel_t::MWT) {
faces = mwt_face(f, superface, stats);
if (faces.size()) {
stats.mwt++;
stats.facemwt += faces.size() - 1;
}
}
// brute force rotating the start point until we find a valid winding
// that doesn't have any T-junctions
if (!faces.size() && qbsp_options.tjunc.value() >= settings::tjunclevel_t::ROTATE) {
size_t i = 0;
for (; i < superface.size(); i++) {
size_t x = 0;
// try vertex i as the base, see if we find any zero-area triangles
for (; x < superface.size() - 2; x++) {
auto v0 = superface[i];
auto v1 = superface[(i + x + 1) % superface.size()];
auto v2 = superface[(i + x + 2) % superface.size()];
if (!TriangleIsValid(v0, v1, v2, 0.01)) {
break;
}
}
if (x == superface.size() - 2) {
// found one!
break;
}
}
if (i == superface.size()) {
// can't simply rotate to eliminate zero-area triangles, so we have
// to do a bit of re-topology.
if (qbsp_options.tjunc.value() >= settings::tjunclevel_t::RETOPOLOGIZE) {
if (auto retopology = RetopologizeFace(f, superface); retopology.size() > 1) {
stats.retopology++;
stats.faceretopology += retopology.size() - 1;
faces = std::move(retopology);
}
}
if (!faces.size()) {
// unable to re-topologize, so just stick with the superface.
// it's got zero-area triangles that fill in the gaps.
stats.norotates++;
}
} else if (i != 0) {
// was able to rotate the superface to eliminate zero-area triangles.
stats.rotates++;
auto &output = faces.emplace_back(superface.size());
// copy base -> end
auto out = std::copy(superface.begin() + i, superface.end(), output.begin());
// copy end -> base
std::copy(superface.begin(), superface.begin() + i, out);
}
}
// the other techniques all failed, or we asked to not
// try them. just move the superface in directly.
if (!faces.size()) {
faces.emplace_back(std::move(superface));
}
Q_assert(faces.size());
// split giant superfaces into subfaces if we have an edge limit.
if (qbsp_options.maxedges.value()) {
for (auto &face : faces) {
Q_assert(face.size() >= 3);
SplitFaceIntoFragments(face, faces, stats);
Q_assert(face.size() >= 3);
}
}
// move the results into the face
f->fragments.reserve(faces.size());
for (auto &face : faces) {
f->fragments.emplace_back(face_fragment_t{std::move(face)});
}
for (auto &frag : f->fragments) {
Q_assert(frag.output_vertices.size() >= 3);
}
}
#include <common/parallel.hh>
/*
==================
FixEdges_r
==================
*/
static void FindFaces_r(node_t *node, std::unordered_set<face_t *> &faces)
{
if (node->is_leaf) {
return;
}
for (auto &f : node->facelist) {
// might have been omitted, so `original_vertices` will be empty
if (f->original_vertices.size()) {
faces.insert(f.get());
}
}
FindFaces_r(node->children[0].get(), faces);
FindFaces_r(node->children[1].get(), faces);
}
/*
===========
TJunc fixing entry point
===========
*/
void TJunc(node_t *headnode)
{
logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__);
tjunc_stats_t stats{};
std::unordered_set<face_t *> faces;
FindFaces_r(headnode, faces);
logging::parallel_for_each(faces, [&](auto &face) { FixFaceEdges(headnode, face, stats); });
if (stats.degenerate) {
logging::print(logging::flag::STAT, "{:5} edges degenerated\n", stats.degenerate);
}
if (stats.facecollapse) {
logging::print(logging::flag::STAT, "{:5} faces degenerated\n", stats.facecollapse);
}
if (stats.tjunctions) {
logging::print(logging::flag::STAT, "{:5} edges added by tjunctions\n", stats.tjunctions);
}
if (stats.mwt) {
logging::print(logging::flag::STAT, "{:5} faces ran through MWT\n", stats.mwt);
logging::print(
logging::flag::STAT, "{:5} new faces added via MWT (from {} triangles)\n", stats.facemwt, stats.trimwt);
}
if (stats.retopology) {
logging::print(logging::flag::STAT, "{:5} faces re-topologized\n", stats.retopology);
logging::print(logging::flag::STAT, "{:5} new faces added by re-topology\n", stats.faceretopology);
}
if (stats.rotates) {
logging::print(logging::flag::STAT, "{:5} faces rotated\n", stats.rotates);
}
if (stats.norotates) {
logging::print(logging::flag::STAT, "{:5} faces unable to be rotated or re-topologized\n", stats.norotates);
}
if (stats.faceoverflows) {
logging::print(logging::flag::STAT, "{:5} faces added by splitting large faces\n", stats.faceoverflows);
}
}