new, simpler qbsp3-esque TJunc code;

- currently uses naive brute force approach to finding vertices on faces
- simplify 'face fragments', which now only need to contain vertex indices since they are already emitted
This commit is contained in:
Jonathan 2022-07-11 01:40:10 -04:00
parent 4feb2bd2c7
commit f98dd05f56
6 changed files with 189 additions and 396 deletions

View File

@ -278,7 +278,7 @@ constexpr int HULL_COLLISION = -1;
void Brush_LoadEntity(mapentity_t *entity, const int hullnum); void Brush_LoadEntity(mapentity_t *entity, const int hullnum);
std::list<face_t *> CSGFace(face_t *srcface, const mapentity_t* srcentity, const bspbrush_t *srcbrush, const node_t *srcnode); std::list<face_t *> CSGFace(face_t *srcface, const mapentity_t* srcentity, const bspbrush_t *srcbrush, const node_t *srcnode);
void TJunc(const mapentity_t *entity, node_t *headnode); void TJunc(node_t *headnode);
int MakeFaceEdges(node_t *headnode); int MakeFaceEdges(node_t *headnode);
void EmitVertices(node_t *headnode); void EmitVertices(node_t *headnode);
void ExportClipNodes(mapentity_t *entity, node_t *headnode, const int hullnum); void ExportClipNodes(mapentity_t *entity, node_t *headnode, const int hullnum);

View File

@ -325,7 +325,7 @@ class mapentity_t;
struct face_fragment_t struct face_fragment_t
{ {
winding_t w; std::vector<size_t> output_vertices; // filled in by EmitVertices & TJunc
std::vector<int64_t> edges; // only filled in MakeFaceEdges std::vector<int64_t> edges; // only filled in MakeFaceEdges
std::optional<size_t> outputnumber; // only valid for original faces after std::optional<size_t> outputnumber; // only valid for original faces after
// write surfaces // write surfaces
@ -340,11 +340,12 @@ struct face_t : face_fragment_t
int texinfo; int texinfo;
contentflags_t contents; // contents on the front of the face contentflags_t contents; // contents on the front of the face
int16_t lmshift; int16_t lmshift;
winding_t w;
qvec3d origin; qvec3d origin;
vec_t radius; vec_t radius;
// filled by TJunc // only valid after tjunction code
std::vector<face_fragment_t> fragments; std::vector<face_fragment_t> fragments;
portal_t *portal; portal_t *portal;

View File

@ -130,6 +130,8 @@ static void AddBounceLight(const qvec3d &pos, const std::map<int, qvec3d> &color
const int lastBounceLightIndex = static_cast<int>(bouncelights.size()) - 1; const int lastBounceLightIndex = static_cast<int>(bouncelights.size()) - 1;
bouncelightsByFacenum[Face_GetNum(bsp, face)].push_back(lastBounceLightIndex); bouncelightsByFacenum[Face_GetNum(bsp, face)].push_back(lastBounceLightIndex);
logging::print("bounce light added: {}\n", colorByStyle.at(0));
} }
const std::vector<bouncelight_t> &BounceLights() const std::vector<bouncelight_t> &BounceLights()

View File

@ -57,7 +57,7 @@ EmitVertex
NOTE: modifies input to be rounded! NOTE: modifies input to be rounded!
============= =============
*/ */
inline void EmitVertex(qvec3d &vert) inline void EmitVertex(qvec3d &vert, size_t &vert_id)
{ {
// if we're extremely close to an integral point, // if we're extremely close to an integral point,
// snap us to it. // snap us to it.
@ -70,11 +70,12 @@ inline void EmitVertex(qvec3d &vert)
// already added // already added
if (auto v = map.find_emitted_hash_vector(vert)) { if (auto v = map.find_emitted_hash_vector(vert)) {
vert_id = *v;
return; return;
} }
// add new vertex! // add new vertex!
map.add_hash_vector(vert, map.bsp.dvertexes.size()); map.add_hash_vector(vert, vert_id = map.bsp.dvertexes.size());
map.bsp.dvertexes.emplace_back(vert); map.bsp.dvertexes.emplace_back(vert);
} }
@ -86,14 +87,10 @@ static void EmitFaceVertices(face_t *f)
return; return;
} }
for (auto &p : f->w) { f->output_vertices.resize(f->w.size());
EmitVertex(p);
}
for (auto &frag : f->fragments) { for (size_t i = 0; i < f->w.size(); i++) {
for (auto &p : frag.w) { EmitVertex(f->w[i], f->output_vertices[i]);
EmitVertex(p);
}
} }
} }
@ -125,21 +122,11 @@ GetEdge
Returns a global edge number, possibly negative to indicate a backwards edge. Returns a global edge number, possibly negative to indicate a backwards edge.
================== ==================
*/ */
inline int64_t GetEdge(const qvec3d &p1, const qvec3d &p2, const face_t *face) inline int64_t GetEdge(const size_t &v1, const size_t &v2, const face_t *face)
{ {
if (!face->contents.is_valid(options.target_game, false)) if (!face->contents.is_valid(options.target_game, false))
FError("Face with invalid contents"); FError("Face with invalid contents");
auto v1o = map.find_emitted_hash_vector(p1);
auto v2o = map.find_emitted_hash_vector(p2);
if (!v1o || !v2o) {
FError("invalid output vertex");
}
size_t v1 = *v1o;
size_t v2 = *v2o;
// search for existing edges // search for existing edges
if (auto it = map.hashedges.find(std::make_pair(v1, v2)); it != map.hashedges.end()) { if (auto it = map.hashedges.find(std::make_pair(v1, v2)); it != map.hashedges.end()) {
return it->second; return it->second;
@ -161,15 +148,15 @@ static void FindFaceFragmentEdges(face_t *face, face_fragment_t *fragment)
{ {
fragment->outputnumber = std::nullopt; fragment->outputnumber = std::nullopt;
if (fragment->w.size() > MAXEDGES) { if (fragment->output_vertices.size() > MAXEDGES) {
FError("Internal error: face->numpoints > MAXEDGES"); FError("Internal error: face->numpoints > MAXEDGES");
} }
fragment->edges.resize(fragment->w.size()); fragment->edges.resize(fragment->output_vertices.size());
for (size_t i = 0; i < fragment->w.size(); i++) { for (size_t i = 0; i < fragment->output_vertices.size(); i++) {
const qvec3d &p1 = fragment->w[i]; auto &p1 = fragment->output_vertices[i];
const qvec3d &p2 = fragment->w[(i + 1) % fragment->w.size()]; auto &p2 = fragment->output_vertices[(i + 1) % fragment->output_vertices.size()];
fragment->edges[i] = GetEdge(p1, p2, face); fragment->edges[i] = GetEdge(p1, p2, face);
} }
} }
@ -237,7 +224,7 @@ static void EmitFaceFragment(face_t *face, face_fragment_t *fragment)
// emit surfedges // emit surfedges
out.firstedge = static_cast<int32_t>(map.bsp.dsurfedges.size()); out.firstedge = static_cast<int32_t>(map.bsp.dsurfedges.size());
std::copy(fragment->edges.cbegin(), fragment->edges.cbegin() + fragment->w.size(), std::copy(fragment->edges.cbegin(), fragment->edges.cbegin() + fragment->output_vertices.size(),
std::back_inserter(map.bsp.dsurfedges)); std::back_inserter(map.bsp.dsurfedges));
fragment->edges.clear(); fragment->edges.clear();

View File

@ -647,7 +647,7 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum)
EmitVertices(tree->headnode.get()); EmitVertices(tree->headnode.get());
if (!options.notjunc.value()) { if (!options.notjunc.value()) {
TJunc(entity, tree->headnode.get()); TJunc(tree->headnode.get());
} }
if (options.objexport.value() && entity == map.world_entity()) { if (options.objexport.value() && entity == map.world_entity()) {

View File

@ -23,381 +23,213 @@
#include <qbsp/qbsp.hh> #include <qbsp/qbsp.hh>
#include <qbsp/map.hh> #include <qbsp/map.hh>
constexpr size_t MAXPOINTS = 60; size_t c_degenerate;
size_t c_tjunctions;
struct wvert_t size_t c_faceoverflows;
{ size_t c_facecollapse;
vec_t t; /* t-value for parametric equation of edge */ size_t c_badstartverts;
wvert_t *prev, *next; /* t-ordered list of vertices on same edge */
};
struct wedge_t
{
wedge_t *next; /* pointer for hash bucket chain */
qvec3d dir; /* direction vector for the edge */
qvec3d origin; /* origin (t = 0) in parametric form */
wvert_t head; /* linked list of verticies on this edge */
};
static int numwedges, numwverts;
static int tjuncs;
static int tjuncfaces;
static int cWVerts;
static int cWEdges;
static std::vector<wvert_t> pWVerts;
static std::vector<wedge_t> pWEdges;
//============================================================================
constexpr size_t NUM_HASH = 1024;
static wedge_t *wedge_hash[NUM_HASH];
static qvec3d hash_min, hash_scale;
static void InitHash(const qvec3d &mins, const qvec3d &maxs)
{
vec_t volume;
vec_t scale;
int newsize[2];
hash_min = mins;
qvec3d size = maxs - mins;
memset(wedge_hash, 0, sizeof(wedge_hash));
volume = size[0] * size[1];
scale = sqrt(volume / NUM_HASH);
newsize[0] = (int)(size[0] / scale);
newsize[1] = (int)(size[1] / scale);
hash_scale[0] = newsize[0] / size[0];
hash_scale[1] = newsize[1] / size[1];
hash_scale[2] = (vec_t)newsize[1];
}
static unsigned HashVec(const qvec3d &vec)
{
unsigned h;
h = (unsigned)(hash_scale[0] * (vec[0] - hash_min[0]) * hash_scale[2] + hash_scale[1] * (vec[1] - hash_min[1]));
if (h >= NUM_HASH)
return NUM_HASH - 1;
return h;
}
//============================================================================
static void CanonicalVector(const qvec3d &p1, const qvec3d &p2, qvec3d &vec)
{
vec = p2 - p1;
vec_t length = qv::normalizeInPlace(vec);
for (size_t i = 0; i < 3; i++) {
if (vec[i] > EQUAL_EPSILON)
return;
else if (vec[i] < -EQUAL_EPSILON) {
vec = -vec;
return;
} else {
vec[i] = 0;
}
}
// FIXME: Line {}: was here but no line number can be grabbed here?
logging::print("WARNING: Healing degenerate edge ({}) at ({:.3})\n", length, p1);
}
static wedge_t *FindEdge(const qvec3d &p1, const qvec3d &p2, vec_t &t1, vec_t &t2)
{
qvec3d origin, edgevec;
wedge_t *edge;
int h;
CanonicalVector(p1, p2, edgevec);
t1 = qv::dot(p1, edgevec);
t2 = qv::dot(p2, edgevec);
origin = p1 + (edgevec * -t1);
if (t1 > t2) {
std::swap(t1, t2);
}
h = HashVec(origin);
for (edge = wedge_hash[h]; edge; edge = edge->next) {
vec_t temp = edge->origin[0] - origin[0];
if (temp < -EQUAL_EPSILON || temp > EQUAL_EPSILON)
continue;
temp = edge->origin[1] - origin[1];
if (temp < -EQUAL_EPSILON || temp > EQUAL_EPSILON)
continue;
temp = edge->origin[2] - origin[2];
if (temp < -EQUAL_EPSILON || temp > EQUAL_EPSILON)
continue;
temp = edge->dir[0] - edgevec[0];
if (temp < -EQUAL_EPSILON || temp > EQUAL_EPSILON)
continue;
temp = edge->dir[1] - edgevec[1];
if (temp < -EQUAL_EPSILON || temp > EQUAL_EPSILON)
continue;
temp = edge->dir[2] - edgevec[2];
if (temp < -EQUAL_EPSILON || temp > EQUAL_EPSILON)
continue;
return edge;
}
if (numwedges >= cWEdges)
FError("Internal error: didn't allocate enough edges for tjuncs?");
edge = &pWEdges[numwedges];
numwedges++;
edge->next = wedge_hash[h];
wedge_hash[h] = edge;
edge->origin = origin;
edge->dir = edgevec;
edge->head.next = edge->head.prev = &edge->head;
edge->head.t = VECT_MAX;
return edge;
}
/* /*
=============== ==========
AddVert TestEdge
=============== Can be recursively reentered
==========
*/ */
static void AddVert(wedge_t *edge, vec_t t) 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)
{ {
wvert_t *v, *newv; if (p1 == p2) {
// degenerate edge
c_degenerate++;
return;
}
v = edge->head.next; for (size_t k = startvert; k < edge_verts.size(); k++) {
do { size_t j = edge_verts[k];
if (fabs(v->t - t) < T_EPSILON)
return;
if (v->t > t)
break;
v = v->next;
} while (1);
// insert a new wvert before v if (j == p1 || j == p2) {
if (numwverts >= cWVerts) continue;
FError("Internal error: didn't allocate enough vertices for tjuncs?"); }
newv = &pWVerts[numwverts]; const qvec3d &p = map.bsp.dvertexes[j];
numwverts++; qvec3d delta = p - edge_start;
vec_t dist = qv::dot(delta, edge_dir);
newv->t = t; // check if off an end
newv->next = v; if (dist <= start || dist >= end) {
newv->prev = v->prev; continue;
v->prev->next = newv; }
v->prev = newv;
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) {
continue; // not on the edge
}
// break the edge
c_tjunctions++;
TestEdge (start, dist, p1, j, k + 1, edge_verts, edge_start, edge_dir, superface);
TestEdge (dist, end, j, p2, k + 1, edge_verts, edge_start, edge_dir, superface);
return;
}
// the edge p1 to p2 is now free of tjunctions
superface.push_back(p1);
} }
/* /*
=============== ==========
AddEdge FindEdgeVerts
=============== Forced a dumb check of everything
==========
*/ */
static void AddEdge(const qvec3d &p1, const qvec3d &p2) static void FindEdgeVerts(const qvec3d &, const qvec3d &, std::vector<size_t> &verts)
{ {
vec_t t1, t2; verts.resize(map.bsp.dvertexes.size() - 1);
wedge_t *edge = FindEdge(p1, p2, t1, t2);
AddVert(edge, t1); for (size_t i = 0; i < verts.size(); i++) {
AddVert(edge, t2); verts[i] = i + 1;
}
} }
/* /*
=============== ==================
AddFaceEdges FaceFromSuperverts
=============== The faces vertexes have been added to the superverts[] array,
and there may be more there than can be held in a face (MAXEDGES).
If less, the faces vertexnums[] will be filled in, otherwise
face will reference a tree of split[] faces until all of the
vertexnums can be added.
superverts[base] will become face->vertexnums[0], and the others
will be circularly filled in.
==================
*/ */
static void AddFaceEdges(face_t *f) inline void FaceFromSuperverts(node_t *node, face_t *f, size_t base, const std::vector<size_t> &superface)
{ {
for (size_t i = 0; i < f->w.size(); i++) { size_t remaining = superface.size();
size_t j = (i + 1) % f->w.size();
AddEdge(f->w[i], f->w[j]);
}
}
//============================================================================ // don't need to do any work
if (remaining <= MAXEDGES) {
return;
}
// If we hit this amount of points, there's probably an issue // split into multiple fragments, because of vertex overload
// in the algorithm that is generating endless vertices. while (remaining > MAXEDGES) {
constexpr size_t MAX_TJUNC_POINTS = 8192; c_faceoverflows++;
static void SplitFaceForTjunc(face_t *face) auto &newf = f->fragments.emplace_back();
{
winding_t &w = face->w;
qvec3d edgevec[2];
vec_t angle;
int i, firstcorner, lastcorner;
do { newf.output_vertices.resize(MAXEDGES);
if (w.size() <= MAXPOINTS) {
// the face is now small enough without more cutting
return;
}
tjuncfaces++; for (size_t i = 0; i < MAXEDGES; i++) {
newf.output_vertices[i] = superface[(i + base) % superface.size()];
}
restart: remaining -= (MAXEDGES - 2);
/* find the last corner */ base = (base + MAXEDGES - 1) % superface.size();
edgevec[0] = qv::normalize(w[w.size() - 1] - w[0]); }
for (lastcorner = w.size() - 1; lastcorner > 0; lastcorner--) {
const qvec3d &p0 = w[lastcorner - 1];
const qvec3d &p1 = w[lastcorner];
edgevec[1] = qv::normalize(p0 - p1);
angle = qv::dot(edgevec[0], edgevec[1]);
if (angle < 1 - ANGLEEPSILON || angle > 1 + ANGLEEPSILON)
break;
}
/* find the first corner */ // copy the vertexes back to the face
edgevec[0] = qv::normalize(w[1] - w[0]); f->w.resize(remaining);
for (firstcorner = 1; firstcorner < w.size() - 1; firstcorner++) {
const qvec3d &p0 = w[firstcorner + 1];
const qvec3d &p1 = w[firstcorner];
edgevec[1] = qv::normalize(p0 - p1);
angle = qv::dot(edgevec[0], edgevec[1]);
if (angle < 1 - ANGLEEPSILON || angle > 1 + ANGLEEPSILON)
break;
}
if (firstcorner + 2 >= MAXPOINTS) { for (size_t i = 0; i < remaining; i++) {
/* rotate the point winding */ f->output_vertices[i] = superface[(i + base) % superface.size()];
qvec3d point0 = w[0]; }
for (i = 1; i < w.size(); i++)
w[i - 1] = w[i];
w[w.size() - 1] = point0;
goto restart;
}
/*
* cut off as big a piece as possible, less than MAXPOINTS, and not
* past lastcorner
*/
winding_t neww(face->w);
if (w.size() - firstcorner <= MAXPOINTS)
neww.resize(firstcorner + 2);
else if (lastcorner + 2 < MAXPOINTS && w.size() - lastcorner <= MAXPOINTS)
neww.resize(lastcorner + 2);
else
neww.resize(MAXPOINTS);
for (i = 0; i < neww.size(); i++)
Q_assert(qv::equalExact(neww[i], w[i]));
for (i = neww.size() - 1; i < w.size(); i++)
w[i - (neww.size() - 2)] = w[i];
w.resize(w.size() - (neww.size() - 2));
face->fragments.push_back(face_fragment_t{std::move(neww)});
} while (1);
} }
/* /*
=============== ==================
FixFaceEdges FixFaceEdges
==================
===============
*/ */
static void FixFaceEdges(face_t *face) static void FixFaceEdges (node_t *node, face_t *f)
{ {
int i, j; std::vector<size_t> count, start;
wedge_t *edge; std::vector<size_t> superface;
wvert_t *v; superface.reserve(64);
vec_t t1, t2; std::vector<size_t> edge_verts;
for (i = 0; i < face->w.size();) { // brushbsp-fixme
j = (i + 1) % face->w.size(); //if (f->merged || f->split[0] || f->split[1])
// return;
edge = FindEdge(face->w[i], face->w[j], t1, t2); for (size_t i = 0; i < f->w.size(); i++) {
auto &p1 = f->w[i];
auto &p2 = f->w[(i + 1) % f->w.size()];
v = edge->head.next; qvec3d edge_start = p1;
while (v->t < t1 + T_EPSILON) qvec3d e2 = p2;
v = v->next;
if (v->t < t2 - T_EPSILON) { FindEdgeVerts (edge_start, e2, edge_verts);
/* insert a new vertex here */
if (face->w.size() >= MAX_TJUNC_POINTS) {
FError("generated too many points (max {})", MAX_TJUNC_POINTS);
}
tjuncs++; vec_t len;
qvec3d edge_dir = qv::normalize(e2 - edge_start, len);
face->w.resize(face->w.size() + 1); start.push_back(superface.size());
TestEdge(0, len, f->output_vertices[i], f->output_vertices[(i + 1) % f->w.size()], 0, edge_verts, edge_start, edge_dir, superface);
std::copy_backward(face->w.begin() + j, face->w.end() - 1, face->w.end()); count.push_back(superface.size() - start[i]);
}
face->w[j] = edge->origin + (edge->dir * v->t); if (superface.size() < 3) {
// entire face collapsed
f->w.clear();
c_facecollapse++;
return;
}
i = 0; // we want to pick a vertex that doesn't have tjunctions
continue; // on either side, which can cause artifacts on trifans,
} // especially underwater
size_t i = 0;
i++; for (; i < f->w.size(); i++) {
} if (count[i] == 1 && count[(i + f->w.size() - 1) % f->w.size()] == 1) {
break;
}
}
// we're good to go! size_t base;
if (face->w.size() <= MAXPOINTS) {
return;
}
/* Too many edges - needs to be split into multiple faces */ if (i == f->w.size()) {
SplitFaceForTjunc(face); c_badstartverts++;
base = 0;
} else {
// rotate the vertex order
base = start[i];
}
// this may fragment the face if > MAXEDGES
FaceFromSuperverts(node, f, base, superface);
} }
//============================================================================ /*
==================
static void tjunc_count_r(node_t *node) FixEdges_r
==================
*/
static void FixEdges_r(node_t *node)
{ {
if (node->planenum == PLANENUM_LEAF) if (node->planenum == PLANENUM_LEAF) {
return; return;
}
for (auto &f : node->facelist) { for (auto &f : node->facelist) {
cWVerts += f->w.size(); // might have been omitted earlier, so `output_vertices` will be empty
} if (f->output_vertices.size()) {
FixFaceEdges(node, f.get());
}
}
tjunc_count_r(node->children[0].get()); FixEdges_r(node->children[0].get());
tjunc_count_r(node->children[1].get()); FixEdges_r(node->children[1].get());
}
static void tjunc_find_r(node_t *node)
{
if (node->planenum == PLANENUM_LEAF)
return;
for (auto &f : node->facelist) {
AddFaceEdges(f.get());
}
tjunc_find_r(node->children[0].get());
tjunc_find_r(node->children[1].get());
}
static void tjunc_fix_r(node_t *node)
{
if (node->planenum == PLANENUM_LEAF)
return;
for (auto &face : node->facelist) {
FixFaceEdges(face.get());
}
tjunc_fix_r(node->children[0].get());
tjunc_fix_r(node->children[1].get());
} }
/* /*
@ -405,51 +237,22 @@ static void tjunc_fix_r(node_t *node)
tjunc tjunc
=========== ===========
*/ */
void TJunc(const mapentity_t *entity, node_t *headnode) void TJunc(node_t *headnode)
{ {
logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__); logging::print(logging::flag::PROGRESS, "---- {} ----\n", __func__);
/* // break edges on tjunctions
* Guess edges = 1/2 verts c_degenerate = 0;
* Verts are arbitrarily multiplied by 2 because there appears to c_facecollapse = 0;
* be a need for them to "grow" slightly. c_tjunctions = 0;
*/ c_faceoverflows = 0;
cWVerts = 0; c_badstartverts = 0;
tjunc_count_r(headnode);
cWEdges = cWVerts;
cWVerts *= 2;
pWVerts.clear(); FixEdges_r (headnode);
pWEdges.clear();
pWVerts.resize(cWVerts);
pWEdges.resize(cWEdges);
qvec3d maxs; logging::print (logging::flag::STAT, "{:5} edges degenerated\n", c_degenerate);
/* logging::print (logging::flag::STAT, "{:5} faces degenerated\n", c_facecollapse);
* identify all points on common edges logging::print (logging::flag::STAT, "{:5} edges added by tjunctions\n", c_tjunctions);
* origin points won't allways be inside the map, so extend the hash area logging::print (logging::flag::STAT, "{:5} faces added by tjunctions\n", c_faceoverflows);
*/ logging::print (logging::flag::STAT, "{:5} bad start verts\n", c_badstartverts);
for (size_t i = 0; i < 3; i++) {
if (fabs(entity->bounds.maxs()[i]) > fabs(entity->bounds.mins()[i]))
maxs[i] = fabs(entity->bounds.maxs()[i]);
else
maxs[i] = fabs(entity->bounds.mins()[i]);
}
qvec3d mins = -maxs;
InitHash(mins, maxs);
numwedges = numwverts = 0;
tjunc_find_r(headnode);
logging::print(logging::flag::STAT, " {:8} world edges\n", numwedges);
logging::print(logging::flag::STAT, " {:8} edge points\n", numwverts);
/* add extra vertexes on edges where needed */
tjuncs = tjuncfaces = 0;
tjunc_fix_r(headnode);
logging::print(logging::flag::STAT, " {:8} edges added by tjunctions\n", tjuncs);
logging::print(logging::flag::STAT, " {:8} faces added by tjunctions\n", tjuncfaces);
} }