// DO NOT EDIT ! // This file is generated using the MantaFlow preprocessor (prep generate). /****************************************************************************** * * MantaFlow fluid solver framework * Copyright 2011 Tobias Pfaff, Nils Thuerey * * This program is free software, distributed under the terms of the * Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 * * Mesh edge collapse and subdivision * ******************************************************************************/ /******************************************************************************/ // Copyright note: // // These functions (C) Chris Wojtan // Long-term goal is to unify with his split&merge codebase // /******************************************************************************/ #include "edgecollapse.h" #include using namespace std; namespace Manta { // 8-point butterfly subdivision scheme (as described by Brochu&Bridson 2009) Vec3 ButterflySubdivision(Mesh &m, const Corner &ca, const Corner &cb) { Vec3 p = m.nodes(m.corners(ca.prev).node).pos + m.nodes(m.corners(ca.next).node).pos; Vec3 q = m.nodes(ca.node).pos + m.nodes(cb.node).pos; Vec3 r = m.nodes(m.corners(m.corners(ca.next).opposite).node).pos + m.nodes(m.corners(m.corners(ca.prev).opposite).node).pos + m.nodes(m.corners(m.corners(cb.next).opposite).node).pos + m.nodes(m.corners(m.corners(cb.prev).opposite).node).pos; return (8 * p + 2 * q - r) / 16.0; } // Modified Butterfly Subdivision Scheme from: // Interpolating Subdivision for Meshes with Arbitrary Topology // Denis Zorin, Peter Schroder, and Wim Sweldens // input the Corner that satisfies the following: // c.prev.node is the extraordinary vertex, // and c.next.node is the other vertex involved in the subdivision Vec3 OneSidedButterflySubdivision(Mesh &m, const int valence, const Corner &c) { Vec3 out; Vec3 p0 = m.nodes(m.corners(c.prev).node).pos; Vec3 p1 = m.nodes(m.corners(c.next).node).pos; if (valence == 3) { Vec3 p2 = m.nodes(c.node).pos; Vec3 p3 = m.nodes(m.corners(m.corners(c.next).opposite).node).pos; out = (5.0 / 12.0) * p1 - (1.0 / 12.0) * (p2 + p3) + 0.75 * p0; } else if (valence == 4) { Vec3 p2 = m.nodes(m.corners(m.corners(c.next).opposite).node).pos; out = 0.375 * p1 - 0.125 * p2 + 0.75 * p0; } else { // rotate around extraordinary vertex, // calculate subdivision weights, // and interpolate vertex position double rv = 1.0 / (double)valence; out = 0.0; int current = c.prev; for (int j = 0; j < valence; j++) { double s = (0.25 + cos(2 * M_PI * j * rv) + 0.5 * cos(4 * M_PI * j * rv)) * rv; Vec3 p = m.nodes(m.corners(m.corners(current).prev).node).pos; out += s * p; current = m.corners(m.corners(m.corners(current).next).opposite).next; } out += 0.75 * m.nodes(m.corners(c.prev).node).pos; } return out; } // Modified Butterfly Subdivision Scheme from: // Interpolating Subdivision for Meshes with Arbitrary Topology // Denis Zorin, Peter Schroder, and Wim Sweldens Vec3 ModifiedButterflySubdivision(Mesh &m, const Corner &ca, const Corner &cb, const Vec3 &fallback) { // calculate the valence of the two parent vertices int start = ca.prev; int current = start; int valenceA = 0; do { valenceA++; int op = m.corners(m.corners(current).next).opposite; if (op < 0) return fallback; current = m.corners(op).next; } while (current != start); start = ca.next; current = start; int valenceB = 0; do { valenceB++; int op = m.corners(m.corners(current).next).opposite; if (op < 0) return fallback; current = m.corners(op).next; } while (current != start); // if both vertices have valence 6, use butterfly subdivision if (valenceA == 6 && valenceB == 6) { return ButterflySubdivision(m, ca, cb); } else if (valenceA == 6) // use a one-sided scheme { return OneSidedButterflySubdivision(m, valenceB, cb); } else if (valenceB == 6) // use a one-sided scheme { return OneSidedButterflySubdivision(m, valenceA, ca); } else // average the results from two one-sided schemes { return 0.5 * (OneSidedButterflySubdivision(m, valenceA, ca) + OneSidedButterflySubdivision(m, valenceB, cb)); } } bool gAbort = false; // collapse an edge on triangle "trinum". // "which" is 0,1, or 2, // where which==0 is the triangle edge from p0 to p1, // which==1 is the triangle edge from p1 to p2, // and which==2 is the triangle edge from p2 to p0, void CollapseEdge(Mesh &m, const int trinum, const int which, const Vec3 &edgevect, const Vec3 &endpoint, vector &deletedNodes, std::map &taintedTris, int &numCollapses, bool doTubeCutting) { if (gAbort) return; // I wanted to draw a pretty picture of an edge collapse, // but I don't know how to make wacky angled lines in ASCII. // Instead, I will show the before case and tell you what needs to be done. // BEFORE: // * // / \. // /C0 \. // / \. // / \. // / B \. // / \. // /C1 C2 \. // P0 *---------------* P1 // \C2 C1 / // \ / // \ A / // \ / // \ / // \C0 / // \ / // * // // We are going to collapse the edge between P0 and P1 // by deleting P1, // and taking all references to P1, // and rerouting them to P0 instead // // What we need to do: // Move position of P0 // Preserve connectivity in both triangles: // (C1.opposite).opposite = C2.o // (C2.opposite).opposite = C1.o // Delete references to Corners of deleted triangles in both P0 and P1's Corner list // Reassign references to P1: // loop through P1 triangles: // rename P1 references to P0 in p lists. // rename Corner.v references // Copy P1's list of Corners over to P0's list of Corners // Delete P1 Corner ca_old[3], cb_old[3]; ca_old[0] = m.corners(trinum, which); ca_old[1] = m.corners(ca_old[0].next); ca_old[2] = m.corners(ca_old[0].prev); bool haveB = false; if (ca_old[0].opposite >= 0) { cb_old[0] = m.corners(ca_old[0].opposite); cb_old[1] = m.corners(cb_old[0].next); cb_old[2] = m.corners(cb_old[0].prev); haveB = true; } if (!haveB) { // for now, don't collapse return; } int P0 = ca_old[2].node; int P1 = ca_old[1].node; /////////////// // avoid creating nonmanifold edges bool nonmanifold = false; bool nonmanifold2 = false; set &ring0 = m.get1Ring(P0).nodes; set &ring1 = m.get1Ring(P1).nodes; // check for intersections of the 1-rings of P0,P1 int cl = 0, commonVert = -1; for (set::iterator it = ring1.begin(); it != ring1.end(); ++it) if (ring0.find(*it) != ring0.end()) { cl++; if (*it != ca_old[0].node && *it != cb_old[0].node) commonVert = *it; } nonmanifold = cl > 2; nonmanifold2 = cl > 3; if (nonmanifold && ca_old[1].opposite >= 0 && cb_old[1].opposite >= 0 && ca_old[2].opposite >= 0 && cb_old[2].opposite >= 0) // collapsing this edge would create a non-manifold edge { if (nonmanifold2) return; bool topTet = false; bool botTet = false; // check if collapsing this edge will collapse a tet. if (m.corners(ca_old[1].opposite).node == m.corners(ca_old[2].opposite).node) botTet = true; if (m.corners(cb_old[1].opposite).node == m.corners(cb_old[2].opposite).node) topTet = true; if (topTet ^ botTet) { // safe pyramid case. // collapse the whole tet! // First collapse the top of the pyramid, // then carry on collapsing the original verts. Corner cc_old[3], cd_old[3]; if (botTet) cc_old[0] = m.corners(ca_old[1].opposite); else // topTet cc_old[0] = cb_old[2]; cc_old[1] = m.corners(cc_old[0].next); cc_old[2] = m.corners(cc_old[0].prev); if (cc_old[0].opposite < 0) return; cd_old[0] = m.corners(cc_old[0].opposite); cd_old[1] = m.corners(cd_old[0].next); cd_old[2] = m.corners(cd_old[0].prev); int P2 = cc_old[2].node; int P3 = cc_old[1].node; // update tri props of all adjacent triangles of P0,P1 (do before CT updates!) for (int i = 0; i < m.numTriChannels(); i++) { }; // TODO: handleTriPropertyEdgeCollapse(trinum, P2,P3, cc_old[0], cd_old[0]); m.mergeNode(P2, P3); // Preserve connectivity in both triangles if (cc_old[1].opposite >= 0) m.corners(cc_old[1].opposite).opposite = cc_old[2].opposite; if (cc_old[2].opposite >= 0) m.corners(cc_old[2].opposite).opposite = cc_old[1].opposite; if (cd_old[1].opposite >= 0) m.corners(cd_old[1].opposite).opposite = cd_old[2].opposite; if (cd_old[2].opposite >= 0) m.corners(cd_old[2].opposite).opposite = cd_old[1].opposite; //////////////////// // mark the two triangles and the one node for deletion int tmpTrinum = cc_old[0].tri; int tmpOthertri = cd_old[0].tri; m.removeTriFromLookup(tmpTrinum); m.removeTriFromLookup(tmpOthertri); taintedTris[tmpTrinum] = true; taintedTris[tmpOthertri] = true; deletedNodes.push_back(P3); numCollapses++; // recompute Corners for triangles A and B if (botTet) ca_old[0] = m.corners(ca_old[2].opposite); else ca_old[0] = m.corners(ca_old[1].prev); ca_old[1] = m.corners(ca_old[0].next); ca_old[2] = m.corners(ca_old[0].prev); cb_old[0] = m.corners(ca_old[0].opposite); cb_old[1] = m.corners(cb_old[0].next); cb_old[2] = m.corners(cb_old[0].prev); /////////////// // avoid creating nonmanifold edges... again ring0 = m.get1Ring(ca_old[2].node).nodes; ring1 = m.get1Ring(ca_old[1].node).nodes; // check for intersections of the 1-rings of P0,P1 cl = 0; for (set::iterator it = ring1.begin(); it != ring1.end(); ++it) if (*it != ca_old[0].node && ring0.find(*it) != ring0.end()) cl++; if (cl > 2) { // nonmanifold // this can happen if collapsing the first tet leads to another similar collapse that // requires the collapse of a tet. for now, just move on and pick this up later. // if the original component was very small, this first collapse could have led to a tiny // piece of nonmanifold geometry. in this case, just delete everything that remains. if (m.corners(ca_old[0].opposite).tri == cb_old[0].tri && m.corners(ca_old[1].opposite).tri == cb_old[0].tri && m.corners(ca_old[2].opposite).tri == cb_old[0].tri) { taintedTris[ca_old[0].tri] = true; taintedTris[cb_old[0].tri] = true; m.removeTriFromLookup(ca_old[0].tri); m.removeTriFromLookup(cb_old[0].tri); deletedNodes.push_back(ca_old[0].node); deletedNodes.push_back(ca_old[1].node); deletedNodes.push_back(ca_old[2].node); } return; } } else if (topTet && botTet && ca_old[1].opposite >= 0 && ca_old[2].opposite >= 0 && cb_old[1].opposite >= 0 && cb_old[2].opposite >= 0) { if (!(m.corners(ca_old[1].opposite).node == m.corners(ca_old[2].opposite).node && m.corners(cb_old[1].opposite).node == m.corners(cb_old[2].opposite).node && (m.corners(ca_old[1].opposite).node == m.corners(cb_old[1].opposite).node || (m.corners(ca_old[1].opposite).node == cb_old[0].node && m.corners(cb_old[1].opposite).node == ca_old[0].node)))) { // just collapse one for now. // collapse the whole tet! // First collapse the top of the pyramid, // then carry on collapsing the original verts. Corner cc_old[3], cd_old[3]; // collapse top { cc_old[0] = m.corners(ca_old[1].opposite); cc_old[1] = m.corners(cc_old[0].next); cc_old[2] = m.corners(cc_old[0].prev); if (cc_old[0].opposite < 0) return; cd_old[0] = m.corners(cc_old[0].opposite); cd_old[1] = m.corners(cd_old[0].next); cd_old[2] = m.corners(cd_old[0].prev); int P2 = cc_old[2].node; int P3 = cc_old[1].node; // update tri props of all adjacent triangles of P0,P1 (do before CT updates!) // TODO: handleTriPropertyEdgeCollapse(trinum, P2,P3, cc_old[0], cd_old[0]); m.mergeNode(P2, P3); // Preserve connectivity in both triangles if (cc_old[1].opposite >= 0) m.corners(cc_old[1].opposite).opposite = cc_old[2].opposite; if (cc_old[2].opposite >= 0) m.corners(cc_old[2].opposite).opposite = cc_old[1].opposite; if (cd_old[1].opposite >= 0) m.corners(cd_old[1].opposite).opposite = cd_old[2].opposite; if (cd_old[2].opposite >= 0) m.corners(cd_old[2].opposite).opposite = cd_old[1].opposite; //////////////////// // mark the two triangles and the one node for deletion int tmpTrinum = cc_old[0].tri; int tmpOthertri = cd_old[0].tri; taintedTris[tmpTrinum] = true; taintedTris[tmpOthertri] = true; m.removeTriFromLookup(tmpTrinum); m.removeTriFromLookup(tmpOthertri); deletedNodes.push_back(P3); numCollapses++; } // then collapse bottom { // cc_old[0] = [ca_old[1].opposite; cc_old[0] = cb_old[2]; cc_old[1] = m.corners(cc_old[0].next); cc_old[2] = m.corners(cc_old[0].prev); if (cc_old[0].opposite < 0) return; cd_old[0] = m.corners(cc_old[0].opposite); cd_old[1] = m.corners(cd_old[0].next); cd_old[2] = m.corners(cd_old[0].prev); int P2 = cc_old[2].node; int P3 = cc_old[1].node; // update tri props of all adjacent triangles of P0,P1 (do before CT updates!) // TODO: handleTriPropertyEdgeCollapse(trinum, P2,P3, cc_old[0], cd_old[0]); m.mergeNode(P2, P3); // Preserve connectivity in both triangles if (cc_old[1].opposite >= 0) m.corners(cc_old[1].opposite).opposite = cc_old[2].opposite; if (cc_old[2].opposite >= 0) m.corners(cc_old[2].opposite).opposite = cc_old[1].opposite; if (cd_old[1].opposite >= 0) m.corners(cd_old[1].opposite).opposite = cd_old[2].opposite; if (cd_old[2].opposite >= 0) m.corners(cd_old[2].opposite).opposite = cd_old[1].opposite; //////////////////// // mark the two triangles and the one node for deletion int tmpTrinum = cc_old[0].tri; int tmpOthertri = cd_old[0].tri; taintedTris[tmpTrinum] = true; taintedTris[tmpOthertri] = true; deletedNodes.push_back(P3); numCollapses++; } // Though we've collapsed a lot of stuff, we still haven't collapsed the original edge. // At this point we still haven't guaranteed that this original collapse weill be safe. // quit for now, and we'll catch the remaining short edges the next time this function is // called. return; } } else if (doTubeCutting) { // tube case // cout<<"CollapseEdge:tube case" << endl; // find the edges that touch the common vert int P2 = commonVert; int P1P2 = -1, P2P1, P2P0 = -1, P0P2 = -1; // corners across from the cutting seam int start = ca_old[0].next; int end = cb_old[0].prev; int current = start; do { // rotate around vertex P1 counter-clockwise int op = m.corners(m.corners(current).next).opposite; if (op < 0) errMsg("tube cutting failed, no opposite"); current = m.corners(op).next; if (m.corners(m.corners(current).prev).node == commonVert) P1P2 = m.corners(current).next; } while (current != end); start = ca_old[0].prev; end = cb_old[0].next; current = start; do { // rotate around vertex P0 clockwise int op = m.corners(m.corners(current).prev).opposite; if (op < 0) errMsg("tube cutting failed, no opposite"); current = m.corners(op).prev; if (m.corners(m.corners(current).next).node == commonVert) P2P0 = m.corners(current).prev; } while (current != end); if (P1P2 < 0 || P2P0 < 0) errMsg("tube cutting failed, ill geometry"); P2P1 = m.corners(P1P2).opposite; P0P2 = m.corners(P2P0).opposite; // duplicate vertices on the top half of the cut, // and use them to split the tube at this seam int P0b = m.addNode(Node(m.nodes(P0).pos)); int P1b = m.addNode(Node(m.nodes(P1).pos)); int P2b = m.addNode(Node(m.nodes(P2).pos)); for (int i = 0; i < m.numNodeChannels(); i++) { m.nodeChannel(i)->addInterpol(P0, P0, 0.5); m.nodeChannel(i)->addInterpol(P1, P1, 0.5); m.nodeChannel(i)->addInterpol(P2, P2, 0.5); } // offset the verts in the normal directions to avoid self intersections Vec3 offsetVec = cross(m.nodes(P1).pos - m.nodes(P0).pos, m.nodes(P2).pos - m.nodes(P0).pos); normalize(offsetVec); offsetVec *= 0.01; // HACK: m.nodes(P0).pos -= offsetVec; m.nodes(P1).pos -= offsetVec; m.nodes(P2).pos -= offsetVec; m.nodes(P0b).pos += offsetVec; m.nodes(P1b).pos += offsetVec; m.nodes(P2b).pos += offsetVec; // create a list of all triangles which touch P0, P1, and P2 from the top, map topTris; start = cb_old[0].next; end = m.corners(P0P2).prev; current = start; topTris[start / 3] = true; do { // rotate around vertex P0 counter-clockwise current = m.corners(m.corners(m.corners(current).next).opposite).next; topTris[current / 3] = true; } while (current != end); start = m.corners(P0P2).next; end = m.corners(P2P1).prev; current = start; topTris[start / 3] = true; do { // rotate around vertex P0 counter-clockwise current = m.corners(m.corners(m.corners(current).next).opposite).next; topTris[current / 3] = true; } while (current != end); start = m.corners(P2P1).next; end = cb_old[0].prev; current = start; topTris[start / 3] = true; do { // rotate around vertex P0 counter-clockwise current = m.corners(m.corners(m.corners(current).next).opposite).next; topTris[current / 3] = true; } while (current != end); // create two new triangles, int Ta = m.addTri(Triangle(P0, P1, P2)); int Tb = m.addTri(Triangle(P1b, P0b, P2b)); for (int i = 0; i < m.numTriChannels(); i++) { m.triChannel(i)->addNew(); m.triChannel(i)->addNew(); } // sew the tris to close the cut on each side for (int c = 0; c < 3; c++) m.addCorner(Corner(Ta, m.tris(Ta).c[c])); for (int c = 0; c < 3; c++) m.addCorner(Corner(Tb, m.tris(Tb).c[c])); for (int c = 0; c < 3; c++) { m.corners(Ta, c).next = 3 * Ta + ((c + 1) % 3); m.corners(Ta, c).prev = 3 * Ta + ((c + 2) % 3); m.corners(Tb, c).next = 3 * Tb + ((c + 1) % 3); m.corners(Tb, c).prev = 3 * Tb + ((c + 2) % 3); } m.corners(Ta, 0).opposite = P1P2; m.corners(Ta, 1).opposite = P2P0; m.corners(Ta, 2).opposite = ca_old[1].prev; m.corners(Tb, 0).opposite = P0P2; m.corners(Tb, 1).opposite = P2P1; m.corners(Tb, 2).opposite = cb_old[1].prev; for (int c = 0; c < 3; c++) { m.corners(m.corners(Ta, c).opposite).opposite = 3 * Ta + c; m.corners(m.corners(Tb, c).opposite).opposite = 3 * Tb + c; } // replace P0,P1,P2 on the top with P0b,P1b,P2b. for (map::iterator tti = topTris.begin(); tti != topTris.end(); tti++) { // cout << "H " << tti->first << " : " << m.tris(tti->first).c[0] << " " << // m.tris(tti->first).c[1] << " " << m.tris(tti->first).c[2] << " " << endl; for (int i = 0; i < 3; i++) { int cn = m.tris(tti->first).c[i]; set &ring = m.get1Ring(cn).nodes; if (ring.find(P0) != ring.end() && cn != P0 && cn != P1 && cn != P2 && cn != P0b && cn != P1b && cn != P2b) { ring.erase(P0); ring.insert(P0b); m.get1Ring(P0).nodes.erase(cn); m.get1Ring(P0b).nodes.insert(cn); } if (ring.find(P1) != ring.end() && cn != P0 && cn != P1 && cn != P2 && cn != P0b && cn != P1b && cn != P2b) { ring.erase(P1); ring.insert(P1b); m.get1Ring(P1).nodes.erase(cn); m.get1Ring(P1b).nodes.insert(cn); } if (ring.find(P2) != ring.end() && cn != P0 && cn != P1 && cn != P2 && cn != P0b && cn != P1b && cn != P2b) { ring.erase(P2); ring.insert(P2b); m.get1Ring(P2).nodes.erase(cn); m.get1Ring(P2b).nodes.insert(cn); } if (cn == P0) { m.tris(tti->first).c[i] = P0b; m.corners(tti->first, i).node = P0b; m.get1Ring(P0).tris.erase(tti->first); m.get1Ring(P0b).tris.insert(tti->first); } else if (cn == P1) { m.tris(tti->first).c[i] = P1b; m.corners(tti->first, i).node = P1b; m.get1Ring(P1).tris.erase(tti->first); m.get1Ring(P1b).tris.insert(tti->first); } else if (cn == P2) { m.tris(tti->first).c[i] = P2b; m.corners(tti->first, i).node = P2b; m.get1Ring(P2).tris.erase(tti->first); m.get1Ring(P2b).tris.insert(tti->first); } } } // m.sanityCheck(true, &deletedNodes, &taintedTris); return; } return; } if (ca_old[1].opposite >= 0 && ca_old[2].opposite >= 0 && cb_old[1].opposite >= 0 && cb_old[2].opposite >= 0 && ca_old[0].opposite >= 0 && cb_old[0].opposite >= 0 && ((m.corners(ca_old[1].opposite).node == m.corners(ca_old[2].opposite).node && // two-pyramid tubey case (6 tris, 5 verts) m.corners(cb_old[1].opposite).node == m.corners(cb_old[2].opposite).node && (m.corners(ca_old[1].opposite).node == m.corners(cb_old[1].opposite).node || (m.corners(ca_old[1].opposite).node == cb_old[0].node && // single tetrahedron case m.corners(cb_old[1].opposite).node == ca_old[0].node))) || (m.corners(ca_old[0].opposite).tri == m.corners(cb_old[0].opposite).tri && m.corners(ca_old[1].opposite).tri == m.corners(cb_old[0].opposite).tri && m.corners(ca_old[2].opposite).tri == m.corners(cb_old[0].opposite).tri // nonmanifold: 2 tris, 3 verts && m.corners(cb_old[0].opposite).tri == m.corners(ca_old[0].opposite).tri && m.corners(cb_old[1].opposite).tri == m.corners(ca_old[0].opposite).tri && m.corners(cb_old[2].opposite).tri == m.corners(ca_old[0].opposite).tri))) { // both top and bottom are closed pyramid caps, or it is a single tet // delete the whole component! // flood fill to mark all triangles in the component map markedTris; queue triQ; triQ.push(trinum); markedTris[trinum] = true; int iters = 0; while (!triQ.empty()) { int trival = triQ.front(); triQ.pop(); for (int i = 0; i < 3; i++) { int newtri = m.corners(m.corners(trival, i).opposite).tri; if (markedTris.find(newtri) == markedTris.end()) { triQ.push(newtri); markedTris[newtri] = true; } } iters++; } map markedverts; for (map::iterator mit = markedTris.begin(); mit != markedTris.end(); mit++) { taintedTris[mit->first] = true; markedverts[m.tris(mit->first).c[0]] = true; markedverts[m.tris(mit->first).c[1]] = true; markedverts[m.tris(mit->first).c[2]] = true; } for (map::iterator mit = markedverts.begin(); mit != markedverts.end(); mit++) deletedNodes.push_back(mit->first); return; } ////////////////////////// // begin original edge collapse // update tri props of all adjacent triangles of P0,P1 (do before CT updates!) // TODO: handleTriPropertyEdgeCollapse(trinum, P0,P1, ca_old[0], cb_old[0]); m.mergeNode(P0, P1); // Move position of P0 m.nodes(P0).pos = endpoint + 0.5 * edgevect; // Preserve connectivity in both triangles if (ca_old[1].opposite >= 0) m.corners(ca_old[1].opposite).opposite = ca_old[2].opposite; if (ca_old[2].opposite >= 0) m.corners(ca_old[2].opposite).opposite = ca_old[1].opposite; if (haveB && cb_old[1].opposite >= 0) m.corners(cb_old[1].opposite).opposite = cb_old[2].opposite; if (haveB && cb_old[2].opposite >= 0) m.corners(cb_old[2].opposite).opposite = cb_old[1].opposite; //////////////////// // mark the two triangles and the one node for deletion taintedTris[ca_old[0].tri] = true; m.removeTriFromLookup(ca_old[0].tri); if (haveB) { taintedTris[cb_old[0].tri] = true; m.removeTriFromLookup(cb_old[0].tri); } deletedNodes.push_back(P1); numCollapses++; } } // namespace Manta