You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

849 lines
33 KiB

// David Eberly, Geometric Tools, Redmond WA 98052
// Copyright (c) 1998-2021
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
// https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt
// Version: 4.0.2019.08.13
#pragma once
#include <Mathematics/Logger.h>
#include <Mathematics/PrimalQuery3.h>
#include <Mathematics/TSManifoldMesh.h>
#include <Mathematics/Line.h>
#include <Mathematics/Hyperplane.h>
#include <set>
#include <vector>
// Delaunay tetrahedralization of points (intrinsic dimensionality 3).
// VQ = number of vertices
// V = array of vertices
// TQ = number of tetrahedra
// I = Array of 4-tuples of indices into V that represent the tetrahedra
// (4*TQ total elements). Access via GetIndices(*).
// A = Array of 4-tuples of indices into I that represent the adjacent
// tetrahedra (4*TQ total elements). Access via GetAdjacencies(*).
// The i-th tetrahedron has vertices
// vertex[0] = V[I[4*i+0]]
// vertex[1] = V[I[4*i+1]]
// vertex[2] = V[I[4*i+2]]
// vertex[3] = V[I[4*i+3]]
// and face index triples listed below. The face vertex ordering when
// viewed from outside the tetrahedron is counterclockwise.
// face[0] = <I[4*i+1],I[4*i+2],I[4*i+3]>
// face[1] = <I[4*i+0],I[4*i+3],I[4*i+2]>
// face[2] = <I[4*i+0],I[4*i+1],I[4*i+3]>
// face[3] = <I[4*i+0],I[4*i+2],I[4*i+1]>
// The tetrahedra adjacent to these faces have indices
// adjacent[0] = A[4*i+0] is the tetrahedron opposite vertex[0], so it
// is the tetrahedron sharing face[0].
// adjacent[1] = A[4*i+1] is the tetrahedron opposite vertex[1], so it
// is the tetrahedron sharing face[1].
// adjacent[2] = A[4*i+2] is the tetrahedron opposite vertex[2], so it
// is the tetrahedron sharing face[2].
// adjacent[3] = A[4*i+3] is the tetrahedron opposite vertex[3], so it
// is the tetrahedron sharing face[3].
// If there is no adjacent tetrahedron, the A[*] value is set to -1. The
// tetrahedron adjacent to face[j] has vertices
// adjvertex[0] = V[I[4*adjacent[j]+0]]
// adjvertex[1] = V[I[4*adjacent[j]+1]]
// adjvertex[2] = V[I[4*adjacent[j]+2]]
// adjvertex[3] = V[I[4*adjacent[j]+3]]
// The only way to ensure a correct result for the input vertices (assumed to
// be exact) is to choose ComputeType for exact rational arithmetic. You may
// use BSNumber. No divisions are performed in this computation, so you do
// not have to use BSRational.
//
// The worst-case choices of N for Real of type BSNumber or BSRational with
// integer storage UIntegerFP32<N> are listed in the next table. The numerical
// computations are encapsulated in PrimalQuery3<Real>::ToPlane and
// PrimalQuery3<Real>::ToCircumsphere, the latter query the dominant one in
// determining N. We recommend using only BSNumber, because no divisions are
// performed in the convex-hull computations.
//
// input type | compute type | N
// -----------+--------------+--------
// float | BSNumber | 44
// float | BSRational | 329
// double | BSNumber | 298037
// double | BSRational | 2254442
namespace gte
{
template <typename InputType, typename ComputeType>
class Delaunay3
{
public:
// The class is a functor to support computing the Delaunay
// tetrahedralization of multiple data sets using the same class
// object.
Delaunay3()
:
mEpsilon((InputType)0),
mDimension(0),
mLine(Vector3<InputType>::Zero(), Vector3<InputType>::Zero()),
mPlane(Vector3<InputType>::Zero(), (InputType)0),
mNumVertices(0),
mNumUniqueVertices(0),
mNumTetrahedra(0),
mVertices(nullptr)
{
}
// The input is the array of vertices whose Delaunay
// tetrahedralization is required. The epsilon value is used to
// determine the intrinsic dimensionality of the vertices
// (d = 0, 1, 2, or 3). When epsilon is positive, the determination
// is fuzzy--vertices approximately the same point, approximately on
// a line, approximately planar, or volumetric.
bool operator()(int numVertices, Vector3<InputType> const* vertices, InputType epsilon)
{
mEpsilon = std::max(epsilon, (InputType)0);
mDimension = 0;
mLine.origin = Vector3<InputType>::Zero();
mLine.direction = Vector3<InputType>::Zero();
mPlane.normal = Vector3<InputType>::Zero();
mPlane.constant = (InputType)0;
mNumVertices = numVertices;
mNumUniqueVertices = 0;
mNumTetrahedra = 0;
mVertices = vertices;
mGraph = TSManifoldMesh();
mIndices.clear();
mAdjacencies.clear();
int i, j;
if (mNumVertices < 4)
{
// Delaunay3 should be called with at least four points.
return false;
}
IntrinsicsVector3<InputType> info(mNumVertices, vertices, mEpsilon);
if (info.dimension == 0)
{
// mDimension is 0; mGraph, mIndices, and mAdjacencies are
// empty
return false;
}
if (info.dimension == 1)
{
// The set is (nearly) collinear.
mDimension = 1;
mLine = Line3<InputType>(info.origin, info.direction[0]);
return false;
}
if (info.dimension == 2)
{
// The set is (nearly) coplanar.
mDimension = 2;
mPlane = Plane3<InputType>(UnitCross(info.direction[0],
info.direction[1]), info.origin);
return false;
}
mDimension = 3;
// Compute the vertices for the queries.
mComputeVertices.resize(mNumVertices);
mQuery.Set(mNumVertices, &mComputeVertices[0]);
for (i = 0; i < mNumVertices; ++i)
{
for (j = 0; j < 3; ++j)
{
mComputeVertices[i][j] = vertices[i][j];
}
}
// Insert the (nondegenerate) tetrahedron constructed by the call
// to GetInformation. This is necessary for the circumsphere
// visibility algorithm to work correctly.
if (!info.extremeCCW)
{
std::swap(info.extreme[2], info.extreme[3]);
}
if (!mGraph.Insert(info.extreme[0], info.extreme[1], info.extreme[2], info.extreme[3]))
{
return false;
}
// Incrementally update the tetrahedralization. The set of
// processed points is maintained to eliminate duplicates, either
// in the original input points or in the points obtained by snap
// rounding.
std::set<Vector3<InputType>> processed;
for (i = 0; i < 4; ++i)
{
processed.insert(vertices[info.extreme[i]]);
}
for (i = 0; i < mNumVertices; ++i)
{
if (processed.find(vertices[i]) == processed.end())
{
if (!Update(i))
{
// A failure can occur if ComputeType is not an exact
// arithmetic type.
return false;
}
processed.insert(vertices[i]);
}
}
mNumUniqueVertices = static_cast<int>(processed.size());
// Assign integer values to the tetrahedra for use by the caller.
std::map<std::shared_ptr<Tetrahedron>, int> permute;
i = -1;
permute[nullptr] = i++;
for (auto const& element : mGraph.GetTetrahedra())
{
permute[element.second] = i++;
}
// Put Delaunay tetrahedra into an array (vertices and adjacency
// info).
mNumTetrahedra = static_cast<int>(mGraph.GetTetrahedra().size());
int numIndices = 4 * mNumTetrahedra;
if (mNumTetrahedra > 0)
{
mIndices.resize(numIndices);
mAdjacencies.resize(numIndices);
i = 0;
for (auto const& element : mGraph.GetTetrahedra())
{
std::shared_ptr<Tetrahedron> tetra = element.second;
for (j = 0; j < 4; ++j, ++i)
{
mIndices[i] = tetra->V[j];
mAdjacencies[i] = permute[tetra->S[j].lock()];
}
}
}
return true;
}
// Dimensional information. If GetDimension() returns 1, the points
// lie on a line P+t*D (fuzzy comparison when epsilon > 0). You can
// sort these if you need a polyline output by projecting onto the
// line each vertex X = P+t*D, where t = Dot(D,X-P). If
// GetDimension() returns 2, the points line on a plane P+s*U+t*V
// (fuzzy comparison when epsilon > 0). You can project each vertex
// X = P+s*U+t*V, where s = Dot(U,X-P) and t = Dot(V,X-P), then apply
// Delaunay2 to the (s,t) tuples.
inline InputType GetEpsilon() const
{
return mEpsilon;
}
inline int GetDimension() const
{
return mDimension;
}
inline Line3<InputType> const& GetLine() const
{
return mLine;
}
inline Plane3<InputType> const& GetPlane() const
{
return mPlane;
}
// Member access.
inline int GetNumVertices() const
{
return mNumVertices;
}
inline int GetNumUniqueVertices() const
{
return mNumUniqueVertices;
}
inline int GetNumTetrahedra() const
{
return mNumTetrahedra;
}
inline Vector3<InputType> const* GetVertices() const
{
return mVertices;
}
inline PrimalQuery3<ComputeType> const& GetQuery() const
{
return mQuery;
}
inline TSManifoldMesh const& GetGraph() const
{
return mGraph;
}
inline std::vector<int> const& GetIndices() const
{
return mIndices;
}
inline std::vector<int> const& GetAdjacencies() const
{
return mAdjacencies;
}
// Locate those tetrahedra faces that do not share other tetrahedra.
// The returned array has hull.size() = 3*numFaces indices, each
// triple representing a triangle. The triangles are counterclockwise
// ordered when viewed from outside the hull. The return value is
// 'true' iff the dimension is 3.
bool GetHull(std::vector<int>& hull) const
{
if (mDimension == 3)
{
// Count the number of triangles that are not shared by two
// tetrahedra.
int numTriangles = 0;
for (auto adj : mAdjacencies)
{
if (adj == -1)
{
++numTriangles;
}
}
if (numTriangles > 0)
{
// Enumerate the triangles. The prototypical case is the
// single tetrahedron V[0] = (0,0,0), V[1] = (1,0,0),
// V[2] = (0,1,0) and V[3] = (0,0,1) with no adjacent
// tetrahedra. The mIndices[] array is <0,1,2,3>.
// i = 0, face = 0:
// skip index 0, <x,1,2,3>, no swap, triangle = <1,2,3>
// i = 1, face = 1:
// skip index 1, <0,x,2,3>, swap, triangle = <0,3,2>
// i = 2, face = 2:
// skip index 2, <0,1,x,3>, no swap, triangle = <0,1,3>
// i = 3, face = 3:
// skip index 3, <0,1,2,x>, swap, triangle = <0,2,1>
// To guarantee counterclockwise order of triangles when
// viewed outside the tetrahedron, the swap of the last
// two indices occurs when face is an odd number;
// (face % 2) != 0.
hull.resize(3 * numTriangles);
int current = 0, i = 0;
for (auto adj : mAdjacencies)
{
if (adj == -1)
{
int tetra = i / 4, face = i % 4;
for (int j = 0; j < 4; ++j)
{
if (j != face)
{
hull[current++] = mIndices[4 * tetra + j];
}
}
if ((face % 2) != 0)
{
std::swap(hull[current - 1], hull[current - 2]);
}
}
++i;
}
return true;
}
else
{
LogError("Unexpected. There must be at least one tetrahedron.");
}
}
else
{
LogError("The dimension must be 3.");
}
}
// Get the vertex indices for tetrahedron i. The function returns
// 'true' when the dimension is 3 and i is a valid tetrahedron index,
// in which case the vertices are valid; otherwise, the function
// returns 'false' and the vertices are invalid.
bool GetIndices(int i, std::array<int, 4>& indices) const
{
if (mDimension == 3)
{
int numTetrahedra = static_cast<int>(mIndices.size() / 4);
if (0 <= i && i < numTetrahedra)
{
indices[0] = mIndices[4 * i];
indices[1] = mIndices[4 * i + 1];
indices[2] = mIndices[4 * i + 2];
indices[3] = mIndices[4 * i + 3];
return true;
}
}
else
{
LogError("The dimension must be 3.");
}
return false;
}
// Get the indices for tetrahedra adjacent to tetrahedron i. The
// function returns 'true' when the dimension is 3 and i is a valid
// tetrahedron index, in which case the adjacencies are valid;
// otherwise, the function returns 'false' and the adjacencies are
// invalid.
bool GetAdjacencies(int i, std::array<int, 4>& adjacencies) const
{
if (mDimension == 3)
{
int numTetrahedra = static_cast<int>(mIndices.size() / 4);
if (0 <= i && i < numTetrahedra)
{
adjacencies[0] = mAdjacencies[4 * i];
adjacencies[1] = mAdjacencies[4 * i + 1];
adjacencies[2] = mAdjacencies[4 * i + 2];
adjacencies[3] = mAdjacencies[4 * i + 3];
return true;
}
}
else
{
LogError("The dimension must be 3.");
}
return false;
}
// Support for searching the tetrahedralization for a tetrahedron
// that contains a point. If there is a containing tetrahedron, the
// returned value is a tetrahedron index i with
// 0 <= i < GetNumTetrahedra(). If there is not a containing
// tetrahedron, -1 is returned. The computations are performed using
// exact rational arithmetic.
//
// The SearchInfo input stores information about the tetrahedron
// search when looking for the tetrahedron (if any) that contains p.
// The first tetrahedron searched is 'initialTetrahedron'. On return
// 'path' stores those (ordered) tetrahedron indices visited during
// the search. The last visited tetrahedron has index
// 'finalTetrahedron' and vertex indices 'finalV[0,1,2,3]', stored in
// volumetric counterclockwise order. The last face of the search is
// <finalV[0],finalV[1],finalV[2]>. For spatially coherent inputs p
// for numerous calls to this function, you will want to specify
// 'finalTetrahedron' from the previous call as 'initialTetrahedron'
// for the next call, which should reduce search times.
struct SearchInfo
{
int initialTetrahedron;
int numPath;
std::vector<int> path;
int finalTetrahedron;
std::array<int, 4> finalV;
};
int GetContainingTetrahedron(Vector3<InputType> const& p, SearchInfo& info) const
{
if (mDimension == 3)
{
Vector3<ComputeType> test{ p[0], p[1], p[2] };
int numTetrahedra = static_cast<int>(mIndices.size() / 4);
info.path.resize(numTetrahedra);
info.numPath = 0;
int tetrahedron;
if (0 <= info.initialTetrahedron
&& info.initialTetrahedron < numTetrahedra)
{
tetrahedron = info.initialTetrahedron;
}
else
{
info.initialTetrahedron = 0;
tetrahedron = 0;
}
// Use tetrahedron faces as binary separating planes.
for (int i = 0; i < numTetrahedra; ++i)
{
int ibase = 4 * tetrahedron;
int const* v = &mIndices[ibase];
info.path[info.numPath++] = tetrahedron;
info.finalTetrahedron = tetrahedron;
info.finalV[0] = v[0];
info.finalV[1] = v[1];
info.finalV[2] = v[2];
info.finalV[3] = v[3];
// <V1,V2,V3> counterclockwise when viewed outside
// tetrahedron.
if (mQuery.ToPlane(test, v[1], v[2], v[3]) > 0)
{
tetrahedron = mAdjacencies[ibase];
if (tetrahedron == -1)
{
info.finalV[0] = v[1];
info.finalV[1] = v[2];
info.finalV[2] = v[3];
info.finalV[3] = v[0];
return -1;
}
continue;
}
// <V0,V3,V2> counterclockwise when viewed outside
// tetrahedron.
if (mQuery.ToPlane(test, v[0], v[2], v[3]) < 0)
{
tetrahedron = mAdjacencies[ibase + 1];
if (tetrahedron == -1)
{
info.finalV[0] = v[0];
info.finalV[1] = v[2];
info.finalV[2] = v[3];
info.finalV[3] = v[1];
return -1;
}
continue;
}
// <V0,V1,V3> counterclockwise when viewed outside
// tetrahedron.
if (mQuery.ToPlane(test, v[0], v[1], v[3]) > 0)
{
tetrahedron = mAdjacencies[ibase + 2];
if (tetrahedron == -1)
{
info.finalV[0] = v[0];
info.finalV[1] = v[1];
info.finalV[2] = v[3];
info.finalV[3] = v[2];
return -1;
}
continue;
}
// <V0,V2,V1> counterclockwise when viewed outside
// tetrahedron.
if (mQuery.ToPlane(test, v[0], v[1], v[2]) < 0)
{
tetrahedron = mAdjacencies[ibase + 3];
if (tetrahedron == -1)
{
info.finalV[0] = v[0];
info.finalV[1] = v[1];
info.finalV[2] = v[2];
info.finalV[3] = v[3];
return -1;
}
continue;
}
return tetrahedron;
}
}
else
{
LogError("The dimension must be 3.");
}
return -1;
}
private:
// Support for incremental Delaunay tetrahedralization.
typedef TSManifoldMesh::Tetrahedron Tetrahedron;
bool GetContainingTetrahedron(int i, std::shared_ptr<Tetrahedron>& tetra) const
{
int numTetrahedra = static_cast<int>(mGraph.GetTetrahedra().size());
for (int t = 0; t < numTetrahedra; ++t)
{
int j;
for (j = 0; j < 4; ++j)
{
auto const& opposite = TetrahedronKey<true>::GetOppositeFace();
int v0 = tetra->V[opposite[j][0]];
int v1 = tetra->V[opposite[j][1]];
int v2 = tetra->V[opposite[j][2]];
if (mQuery.ToPlane(i, v0, v1, v2) > 0)
{
// Point i sees face <v0,v1,v2> from outside the
// tetrahedron.
auto adjTetra = tetra->S[j].lock();
if (adjTetra)
{
// Traverse to the tetrahedron sharing the face.
tetra = adjTetra;
break;
}
else
{
// We reached a hull face, so the point is outside
// the hull. TODO: Once a hull data structure is
// in place, return tetra->S[j] as the candidate
// for starting a search for visible hull faces.
return false;
}
}
}
if (j == 4)
{
// The point is inside all four faces, so the point is inside
// a tetrahedron.
return true;
}
}
LogError("Unexpected termination of loop.");
}
bool GetAndRemoveInsertionPolyhedron(int i, std::set<std::shared_ptr<Tetrahedron>>& candidates,
std::set<TriangleKey<true>>& boundary)
{
// Locate the tetrahedra that make up the insertion polyhedron.
TSManifoldMesh polyhedron;
while (candidates.size() > 0)
{
std::shared_ptr<Tetrahedron> tetra = *candidates.begin();
candidates.erase(candidates.begin());
for (int j = 0; j < 4; ++j)
{
auto adj = tetra->S[j].lock();
if (adj && candidates.find(adj) == candidates.end())
{
int a0 = adj->V[0];
int a1 = adj->V[1];
int a2 = adj->V[2];
int a3 = adj->V[3];
if (mQuery.ToCircumsphere(i, a0, a1, a2, a3) <= 0)
{
// Point i is in the circumsphere.
candidates.insert(adj);
}
}
}
int v0 = tetra->V[0];
int v1 = tetra->V[1];
int v2 = tetra->V[2];
int v3 = tetra->V[3];
if (!polyhedron.Insert(v0, v1, v2, v3))
{
return false;
}
if (!mGraph.Remove(v0, v1, v2, v3))
{
return false;
}
}
// Get the boundary triangles of the insertion polyhedron.
for (auto const& element : polyhedron.GetTetrahedra())
{
std::shared_ptr<Tetrahedron> tetra = element.second;
for (int j = 0; j < 4; ++j)
{
if (!tetra->S[j].lock())
{
auto const& opposite = TetrahedronKey<true>::GetOppositeFace();
int v0 = tetra->V[opposite[j][0]];
int v1 = tetra->V[opposite[j][1]];
int v2 = tetra->V[opposite[j][2]];
boundary.insert(TriangleKey<true>(v0, v1, v2));
}
}
}
return true;
}
bool Update(int i)
{
auto const& smap = mGraph.GetTetrahedra();
std::shared_ptr<Tetrahedron> tetra = smap.begin()->second;
if (GetContainingTetrahedron(i, tetra))
{
// The point is inside the convex hull. The insertion
// polyhedron contains only tetrahedra in the current
// tetrahedralization; the hull does not change.
// Use a depth-first search for those tetrahedra whose
// circumspheres contain point i.
std::set<std::shared_ptr<Tetrahedron>> candidates;
candidates.insert(tetra);
// Get the boundary of the insertion polyhedron C that
// contains the tetrahedra whose circumspheres contain point
// i. Polyhedron C contains the point i.
std::set<TriangleKey<true>> boundary;
if (!GetAndRemoveInsertionPolyhedron(i, candidates, boundary))
{
return false;
}
// The insertion polyhedron consists of the tetrahedra formed
// by point i and the faces of C.
for (auto const& key : boundary)
{
int v0 = key.V[0];
int v1 = key.V[1];
int v2 = key.V[2];
if (mQuery.ToPlane(i, v0, v1, v2) < 0)
{
if (!mGraph.Insert(i, v0, v1, v2))
{
return false;
}
}
// else: Point i is on an edge or face of 'tetra', so the
// subdivision has degenerate tetrahedra. Ignore these.
}
}
else
{
// The point is outside the convex hull. The insertion
// polyhedron is formed by point i and any tetrahedra in the
// current tetrahedralization whose circumspheres contain
// point i.
// Locate the convex hull of the tetrahedra. TODO: Maintain
// a hull data structure that is updated incrementally.
std::set<TriangleKey<true>> hull;
for (auto const& element : smap)
{
std::shared_ptr<Tetrahedron> t = element.second;
for (int j = 0; j < 4; ++j)
{
if (!t->S[j].lock())
{
auto const& opposite = TetrahedronKey<true>::GetOppositeFace();
int v0 = t->V[opposite[j][0]];
int v1 = t->V[opposite[j][1]];
int v2 = t->V[opposite[j][2]];
hull.insert(TriangleKey<true>(v0, v1, v2));
}
}
}
// TODO: Until the hull change, for now just iterate over all
// the hull faces and use the ones visible to point i to
// locate the insertion polyhedron.
auto const& tmap = mGraph.GetTriangles();
std::set<std::shared_ptr<Tetrahedron>> candidates;
std::set<TriangleKey<true>> visible;
for (auto const& key : hull)
{
int v0 = key.V[0];
int v1 = key.V[1];
int v2 = key.V[2];
if (mQuery.ToPlane(i, v0, v1, v2) > 0)
{
auto iter = tmap.find(TriangleKey<false>(v0, v1, v2));
if (iter != tmap.end() && iter->second->T[1].lock() == nullptr)
{
auto adj = iter->second->T[0].lock();
if (adj && candidates.find(adj) == candidates.end())
{
int a0 = adj->V[0];
int a1 = adj->V[1];
int a2 = adj->V[2];
int a3 = adj->V[3];
if (mQuery.ToCircumsphere(i, a0, a1, a2, a3) <= 0)
{
// Point i is in the circumsphere.
candidates.insert(adj);
}
else
{
// Point i is not in the circumsphere but
// the hull face is visible.
visible.insert(key);
}
}
}
else
{
// TODO: Add a preprocessor symbol to this file
// to allow throwing an exception. Currently, the
// code exits gracefully when floating-point
// rounding causes problems.
//
// LogError("Unexpected condition (ComputeType not exact?)");
return false;
}
}
}
// Get the boundary of the insertion subpolyhedron C that
// contains the tetrahedra whose circumspheres contain
// point i.
std::set<TriangleKey<true>> boundary;
if (!GetAndRemoveInsertionPolyhedron(i, candidates, boundary))
{
return false;
}
// The insertion polyhedron P consists of the tetrahedra
// formed by point i and the back faces of C *and* the visible
// faces of mGraph-C.
for (auto const& key : boundary)
{
int v0 = key.V[0];
int v1 = key.V[1];
int v2 = key.V[2];
if (mQuery.ToPlane(i, v0, v1, v2) < 0)
{
// This is a back face of the boundary.
if (!mGraph.Insert(i, v0, v1, v2))
{
return false;
}
}
}
for (auto const& key : visible)
{
if (!mGraph.Insert(i, key.V[0], key.V[2], key.V[1]))
{
return false;
}
}
}
return true;
}
// The epsilon value is used for fuzzy determination of intrinsic
// dimensionality. If the dimension is 0, 1, or 2, the constructor
// returns early. The caller is responsible for retrieving the
// dimension and taking an alternate path should the dimension be
// smaller than 3. If the dimension is 0, the caller may as well
// treat all vertices[] as a single point, say, vertices[0]. If the
// dimension is 1, the caller can query for the approximating line
// and project vertices[] onto it for further processing. If the
// dimension is 2, the caller can query for the approximating plane
// and project vertices[] onto it for further processing.
InputType mEpsilon;
int mDimension;
Line3<InputType> mLine;
Plane3<InputType> mPlane;
// The array of vertices used for geometric queries. If you want to
// be certain of a correct result, choose ComputeType to be BSNumber.
std::vector<Vector3<ComputeType>> mComputeVertices;
PrimalQuery3<ComputeType> mQuery;
// The graph information.
int mNumVertices;
int mNumUniqueVertices;
int mNumTetrahedra;
Vector3<InputType> const* mVertices;
TSManifoldMesh mGraph;
std::vector<int> mIndices;
std::vector<int> mAdjacencies;
};
}