openfoam/applications/utilities/surface/surfaceCheck/surfaceCheck.C
Mark Olesen 779a2ca084 ENH: adjust fileName methods for similarity to std::filesystem::path
- stem(), replace_name(), replace_ext(), remove_ext() etc

- string::contains() method - similar to C++23 method

  Eg,
      if (keyword.contains('/')) ...
  vs
      if (keyword.find('/') != std::string::npos) ...
2022-10-11 17:58:22 +02:00

1089 lines
29 KiB
C

/*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration |
\\ / A nd | www.openfoam.com
\\/ M anipulation |
-------------------------------------------------------------------------------
Copyright (C) 2011-2016 OpenFOAM Foundation
Copyright (C) 2016-2022 OpenCFD Ltd.
-------------------------------------------------------------------------------
License
This file is part of OpenFOAM.
OpenFOAM 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 3 of the License, or
(at your option) any later version.
OpenFOAM 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 OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
Application
surfaceCheck
Group
grpSurfaceUtilities
Description
Check geometric and topological quality of a surface.
Usage
\b surfaceCheck [OPTION] surfaceFile
Options:
- \par -checkSelfIntersection
Check for self-intersection.
- \par -splitNonManifold
Split surface along non-manifold edges.
- \par -verbose
Extra verbosity.
- \par -blockMesh
Write vertices/blocks for tight-fitting 1 cell blockMeshDict.
- \par -outputThreshold \<num files\>
Upper limit on the number of files written.
Prevent surfaces with many disconnected parts from writing many files.
The default is 10. A value of 0 suppresses file writing.
\*---------------------------------------------------------------------------*/
#include "triangle.H"
#include "edgeHashes.H"
#include "triSurface.H"
#include "triSurfaceSearch.H"
#include "triSurfaceTools.H"
#include "argList.H"
#include "OFstream.H"
#include "OBJstream.H"
#include "SortableList.H"
#include "PatchTools.H"
#include "vtkSurfaceWriter.H"
#include "functionObject.H"
#include "DynamicField.H"
#include "edgeMesh.H"
using namespace Foam;
labelList countBins
(
const scalar min,
const scalar max,
const label nBins,
const scalarField& vals
)
{
scalar dist = nBins/(max - min);
labelList binCount(nBins, Zero);
forAll(vals, i)
{
scalar val = vals[i];
label index = -1;
if (Foam::mag(val - min) < SMALL)
{
index = 0;
}
else if (val >= max - SMALL)
{
index = nBins - 1;
}
else
{
index = label((val - min)*dist);
if ((index < 0) || (index >= nBins))
{
WarningInFunction
<< "value " << val << " at index " << i
<< " outside range " << min << " .. " << max << endl;
if (index < 0)
{
index = 0;
}
else
{
index = nBins - 1;
}
}
}
binCount[index]++;
}
return binCount;
}
void writeZoning
(
surfaceWriter& writer,
const triSurface& surf,
const labelList& faceZone,
const word& fieldName,
const fileName& surfFilePath,
const word& surfFileStem
)
{
// Transcribe faces
faceList faces;
surf.triFaceFaces(faces);
writer.open
(
surf.points(),
faces,
(surfFilePath / surfFileStem),
false // serial - already merged
);
fileName outputName = writer.write(fieldName, labelField(faceZone));
writer.clear();
Info<< "Wrote zoning to " << outputName << nl << endl;
}
void writeParts
(
const triSurface& surf,
const label nFaceZones,
const labelList& faceZone,
const fileName& surfFilePath,
const word& surfFileStem
)
{
for (label zone = 0; zone < nFaceZones; zone++)
{
boolList includeMap(surf.size(), false);
forAll(faceZone, facei)
{
if (faceZone[facei] == zone)
{
includeMap[facei] = true;
}
}
triSurface subSurf(surf.subsetMesh(includeMap));
fileName subName
(
surfFilePath
/ surfFileStem + "_" + name(zone) + ".obj"
);
Info<< "writing part " << zone << " size " << subSurf.size()
<< " to " << subName << endl;
subSurf.write(subName);
}
}
void syncEdges(const triSurface& p, labelHashSet& markedEdges)
{
// See comment below about having duplicate edges
const edgeList& edges = p.edges();
edgeHashSet edgeSet(2*markedEdges.size());
for (const label edgei : markedEdges)
{
edgeSet.insert(edges[edgei]);
}
forAll(edges, edgei)
{
if (edgeSet.found(edges[edgei]))
{
markedEdges.insert(edgei);
}
}
}
void syncEdges(const triSurface& p, boolList& isMarkedEdge)
{
// See comment below about having duplicate edges
const edgeList& edges = p.edges();
label n = 0;
forAll(isMarkedEdge, edgei)
{
if (isMarkedEdge[edgei])
{
n++;
}
}
edgeHashSet edgeSet(2*n);
forAll(isMarkedEdge, edgei)
{
if (isMarkedEdge[edgei])
{
edgeSet.insert(edges[edgei]);
}
}
forAll(edges, edgei)
{
if (edgeSet.found(edges[edgei]))
{
isMarkedEdge[edgei] = true;
}
}
}
void writeEdgeSet
(
const word& setName,
const triSurface& surf,
const labelUList& edgeSet
)
{
// Get compact edge mesh
labelList pointToCompact(surf.nPoints(), -1);
DynamicField<point> compactPoints(edgeSet.size());
DynamicList<edge> compactEdges(edgeSet.size());
for (label edgei : edgeSet)
{
const edge& e = surf.edges()[edgei];
edge compactEdge(-1, -1);
forAll(e, ep)
{
label& compacti = pointToCompact[e[ep]];
if (compacti == -1)
{
compacti = compactPoints.size();
label pointi = surf.meshPoints()[e[ep]];
compactPoints.append(surf.points()[pointi]);
}
compactEdge[ep] = compacti;
}
compactEdges.append(compactEdge);
}
edgeMesh eMesh(std::move(compactPoints), std::move(compactEdges));
eMesh.write(setName);
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
int main(int argc, char *argv[])
{
argList::addNote
(
"Check geometric and topological quality of a surface"
);
argList::noParallel();
argList::addArgument("input", "The input surface file");
argList::addBoolOption
(
"checkSelfIntersection",
"Also check for self-intersection"
);
argList::addBoolOption
(
"splitNonManifold",
"Split surface along non-manifold edges "
"(default split is fully disconnected)"
);
argList::addVerboseOption
(
"Additional verbosity"
);
argList::addBoolOption
(
"blockMesh",
"Write vertices/blocks for blockMeshDict"
);
argList::addOption
(
"outputThreshold",
"number",
"Upper limit on the number of files written. "
"Default is 10, using 0 suppresses file writing."
);
argList::addOption
(
"writeSets",
"surfaceFormat",
"Reconstruct and write problem triangles/edges in selected format"
);
argList args(argc, argv);
const auto surfName = args.get<fileName>(1);
const bool checkSelfIntersect = args.found("checkSelfIntersection");
const bool splitNonManifold = args.found("splitNonManifold");
const label outputThreshold =
args.getOrDefault<label>("outputThreshold", 10);
const word surfaceFormat = args.getOrDefault<word>("writeSets", "");
const bool writeSets = !surfaceFormat.empty();
autoPtr<surfaceWriter> surfWriter;
word edgeFormat;
if (writeSets)
{
surfWriter = surfaceWriter::New(surfaceFormat);
// Option1: hard-coded format
edgeFormat = "obj";
//// Option2: same type as surface format. Problem is e.g. .obj format
//// is not a surfaceWriter format.
//edgeFormat = surfaceFormat;
//if (!edgeMesh::canWriteType(edgeFormat))
//{
// edgeFormat = "obj";
//}
}
Info<< "Reading surface from "
<< args.relativePath(surfName) << " ..." << nl << endl;
// Read
// ~~~~
triSurface surf(surfName);
Info<< "Statistics:" << endl;
surf.writeStats(Info);
Info<< endl;
// Split into path and stem (no extension)
const fileName surfFilePath(surfName.path());
word surfFileStem(surfName.stem());
// If originally ".gz", need to strip extension again
if (surfName.has_ext("gz"))
{
surfFileStem.remove_ext();
}
// write bounding box corners
if (args.found("blockMesh"))
{
pointField cornerPts(boundBox(surf.points(), false).points());
Info<< "// blockMeshDict info" << nl << nl;
Info<< "vertices\n(" << nl;
forAll(cornerPts, pti)
{
Info<< " " << cornerPts[pti] << nl;
}
// number of divisions needs adjustment later
Info<< ");\n" << nl
<< "blocks\n"
<< "(\n"
<< " hex (0 1 2 3 4 5 6 7) (10 10 10) simpleGrading (1 1 1)\n"
<< ");\n" << nl;
Info<< "edges\n();" << nl
<< "patches\n();" << endl;
Info<< nl << "// end blockMeshDict info" << nl << endl;
}
// Region sizes
// ~~~~~~~~~~~~
{
labelList regionSize(surf.patches().size(), Zero);
forAll(surf, facei)
{
label region = surf[facei].region();
if (region < 0 || region >= regionSize.size())
{
WarningInFunction
<< "Triangle " << facei << " vertices " << surf[facei]
<< " has region " << region << " which is outside the range"
<< " of regions 0.." << surf.patches().size()-1
<< endl;
}
else
{
regionSize[region]++;
}
}
Info<< "Region\tSize" << nl
<< "------\t----" << nl;
forAll(surf.patches(), patchi)
{
Info<< surf.patches()[patchi].name() << '\t'
<< regionSize[patchi] << nl;
}
Info<< nl << endl;
}
// Check triangles
// ~~~~~~~~~~~~~~~
{
DynamicList<label> illegalFaces(surf.size()/100 + 1);
forAll(surf, facei)
{
// Check silently
if (!triSurfaceTools::validTri(surf, facei, false))
{
illegalFaces.append(facei);
}
}
if (illegalFaces.size())
{
Info<< "Surface has " << illegalFaces.size()
<< " illegal triangles." << endl;
if (surfWriter)
{
boolList isIllegalFace(surf.size(), false);
UIndirectList<bool>(isIllegalFace, illegalFaces) = true;
triSurface subSurf(surf.subsetMesh(isIllegalFace));
// Transcribe faces
faceList faces;
subSurf.triFaceFaces(faces);
surfWriter->open
(
subSurf.points(),
faces,
(surfFilePath / surfFileStem),
false // serial - already merged
);
fileName outputName = surfWriter->write
(
"illegal",
scalarField(subSurf.size(), Zero)
);
surfWriter->clear();
Info<< "Wrote illegal triangles to "
<< outputName << nl << endl;
}
else if (outputThreshold > 0)
{
forAll(illegalFaces, i)
{
// Force warning message
triSurfaceTools::validTri(surf, illegalFaces[i], true);
if (i >= outputThreshold)
{
Info<< "Suppressing further warning messages."
<< " Use -outputThreshold to increase number"
<< " of warnings." << endl;
break;
}
}
OFstream str("illegalFaces");
Info<< "Dumping conflicting face labels to " << str.name()
<< endl
<< "Paste this into the input for surfaceSubset" << endl;
str << illegalFaces;
}
}
else
{
Info<< "Surface has no illegal triangles." << endl;
}
Info<< endl;
}
// Triangle quality
// ~~~~~~~~~~~~~~~~
{
scalarField triQ(surf.size(), Zero);
forAll(surf, facei)
{
const labelledTri& f = surf[facei];
if (f[0] == f[1] || f[0] == f[2] || f[1] == f[2])
{
//WarningInFunction
// << "Illegal triangle " << facei << " vertices " << f
// << " coords " << f.points(surf.points()) << endl;
}
else
{
triQ[facei] = triPointRef
(
surf.points()[f[0]],
surf.points()[f[1]],
surf.points()[f[2]]
).quality();
}
}
labelList binCount = countBins(0, 1, 20, triQ);
Info<< "Triangle quality (equilateral=1, collapsed=0):"
<< endl;
OSstream& os = Info;
os.width(4);
scalar dist = (1.0 - 0.0)/20.0;
scalar min = 0;
forAll(binCount, bini)
{
Info<< " " << min << " .. " << min+dist << " : "
<< 1.0/surf.size() * binCount[bini]
<< endl;
min += dist;
}
Info<< endl;
labelPair minMaxIds = findMinMax(triQ);
const label minIndex = minMaxIds.first();
const label maxIndex = minMaxIds.second();
Info<< " min " << triQ[minIndex] << " for triangle " << minIndex
<< nl
<< " max " << triQ[maxIndex] << " for triangle " << maxIndex
<< nl
<< endl;
if (triQ[minIndex] < SMALL)
{
WarningInFunction
<< "Minimum triangle quality is "
<< triQ[minIndex] << ". This might give problems in"
<< " self-intersection testing later on." << endl;
}
// Dump for subsetting
if (surfWriter)
{
// Transcribe faces
faceList faces(surf.size());
surf.triFaceFaces(faces);
surfWriter->open
(
surf.points(),
faces,
(surfFilePath / surfFileStem),
false // serial - already merged
);
fileName outputName = surfWriter->write("quality", triQ);
surfWriter->clear();
Info<< "Wrote triangle-quality to "
<< outputName << nl << endl;
}
else if (outputThreshold > 0)
{
DynamicList<label> problemFaces(surf.size()/100+1);
forAll(triQ, facei)
{
if (triQ[facei] < 1e-11)
{
problemFaces.append(facei);
}
}
if (!problemFaces.empty())
{
OFstream str("badFaces");
Info<< "Dumping bad quality faces to " << str.name() << endl
<< "Paste this into the input for surfaceSubset" << nl
<< nl << endl;
str << problemFaces;
}
}
}
// Edges
// ~~~~~
{
const edgeList& edges = surf.edges();
const pointField& localPoints = surf.localPoints();
scalarField edgeMag(edges.size());
forAll(edges, edgei)
{
edgeMag[edgei] = edges[edgei].mag(localPoints);
}
labelPair minMaxIds = findMinMax(edgeMag);
const label minEdgei = minMaxIds.first();
const label maxEdgei = minMaxIds.second();
const edge& minE = edges[minEdgei];
const edge& maxE = edges[maxEdgei];
Info<< "Edges:" << nl
<< " min " << edgeMag[minEdgei] << " for edge " << minEdgei
<< " points " << localPoints[minE[0]] << localPoints[minE[1]]
<< nl
<< " max " << edgeMag[maxEdgei] << " for edge " << maxEdgei
<< " points " << localPoints[maxE[0]] << localPoints[maxE[1]]
<< nl
<< endl;
}
// Close points
// ~~~~~~~~~~~~
{
const edgeList& edges = surf.edges();
const pointField& localPoints = surf.localPoints();
const boundBox bb(localPoints);
scalar smallDim = 1e-6 * bb.mag();
Info<< "Checking for points less than 1e-6 of bounding box ("
<< bb.span() << " metre) apart."
<< endl;
// Sort points
SortableList<scalar> sortedMag(mag(localPoints));
label nClose = 0;
for (label i = 1; i < sortedMag.size(); i++)
{
label pti = sortedMag.indices()[i];
label prevPti = sortedMag.indices()[i-1];
if (mag(localPoints[pti] - localPoints[prevPti]) < smallDim)
{
// Check if neighbours.
const labelList& pEdges = surf.pointEdges()[pti];
label edgei = -1;
forAll(pEdges, i)
{
const edge& e = edges[pEdges[i]];
if (e[0] == prevPti || e[1] == prevPti)
{
// point1 and point0 are connected through edge.
edgei = pEdges[i];
break;
}
}
if (nClose < outputThreshold)
{
if (edgei == -1)
{
Info<< " close unconnected points "
<< pti << ' ' << localPoints[pti]
<< " and " << prevPti << ' '
<< localPoints[prevPti]
<< " distance:"
<< mag(localPoints[pti] - localPoints[prevPti])
<< endl;
}
else
{
Info<< " small edge between points "
<< pti << ' ' << localPoints[pti]
<< " and " << prevPti << ' '
<< localPoints[prevPti]
<< " distance:"
<< mag(localPoints[pti] - localPoints[prevPti])
<< endl;
}
}
else if (nClose == outputThreshold)
{
Info<< "Suppressing further warning messages."
<< " Use -outputThreshold to increase number"
<< " of warnings." << endl;
}
nClose++;
}
}
Info<< "Found " << nClose << " nearby points." << nl
<< endl;
}
// Check manifold
// ~~~~~~~~~~~~~~
DynamicList<label> problemFaces(surf.size()/100 + 1);
DynamicList<label> openEdges(surf.nEdges()/100 + 1);
DynamicList<label> multipleEdges(surf.nEdges()/100 + 1);
const labelListList& edgeFaces = surf.edgeFaces();
forAll(edgeFaces, edgei)
{
const labelList& myFaces = edgeFaces[edgei];
if (myFaces.size() == 1)
{
problemFaces.append(myFaces[0]);
openEdges.append(edgei);
}
}
forAll(edgeFaces, edgei)
{
const labelList& myFaces = edgeFaces[edgei];
if (myFaces.size() > 2)
{
forAll(myFaces, myFacei)
{
problemFaces.append(myFaces[myFacei]);
}
multipleEdges.append(edgei);
}
}
problemFaces.shrink();
if (openEdges.size() || multipleEdges.size())
{
Info<< "Surface is not closed since not all edges ("
<< edgeFaces.size() << ") connected to "
<< "two faces:" << endl
<< " connected to one face : " << openEdges.size() << endl
<< " connected to >2 faces : " << multipleEdges.size() << endl;
Info<< "Conflicting face labels:" << problemFaces.size() << endl;
if (!edgeFormat.empty() && openEdges.size())
{
const fileName outputName
(
surfName.lessExt()
+ "_open."
+ edgeFormat
);
Info<< "Writing open edges to "
<< args.relativePath(outputName) << " ..." << endl;
writeEdgeSet(outputName, surf, openEdges);
}
if (!edgeFormat.empty() && multipleEdges.size())
{
const fileName outputName
(
surfName.lessExt()
+ "_multiply."
+ edgeFormat
);
Info<< "Writing multiply connected edges to "
<< args.relativePath(outputName) << " ..." << endl;
writeEdgeSet(outputName, surf, multipleEdges);
}
}
else
{
Info<< "Surface is closed. All edges connected to two faces." << endl;
}
Info<< endl;
// Check singly connected domain
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{
boolList borderEdge(surf.nEdges(), false);
if (splitNonManifold)
{
forAll(edgeFaces, edgei)
{
if (edgeFaces[edgei].size() > 2)
{
borderEdge[edgei] = true;
}
}
syncEdges(surf, borderEdge);
}
labelList faceZone;
label numZones = surf.markZones(borderEdge, faceZone);
Info<< "Number of unconnected parts : " << numZones << endl;
if (numZones > 1 && outputThreshold > 0)
{
Info<< "Splitting surface into parts ..." << endl << endl;
if (!surfWriter)
{
surfWriter.reset(new surfaceWriters::vtkWriter());
}
writeZoning
(
*surfWriter,
surf,
faceZone,
"zone",
surfFilePath,
surfFileStem
);
if (numZones > outputThreshold)
{
Info<< "Limiting number of files to " << outputThreshold
<< endl;
}
writeParts
(
surf,
min(outputThreshold, numZones),
faceZone,
surfFilePath,
surfFileStem
);
}
}
// Check orientation
// ~~~~~~~~~~~~~~~~~
labelHashSet borderEdge(surf.size()/1000);
PatchTools::checkOrientation(surf, false, &borderEdge);
// Bit strange: if a triangle has two same vertices (illegal!) it will
// still have three distinct edges (two of which have the same vertices).
// In this case the faceEdges addressing is not symmetric, i.e. a
// neighbouring, valid, triangle will have correct addressing so 3 distinct
// edges so it will miss one of those two identical edges.
// - we don't want to fix this in PrimitivePatch since it is too specific
// - instead just make sure we mark all identical edges consistently
// when we use them for marking.
syncEdges(surf, borderEdge);
//
// Colour all faces into zones using borderEdge
//
labelList normalZone;
label numNormalZones = PatchTools::markZones(surf, borderEdge, normalZone);
Info<< endl
<< "Number of zones (connected area with consistent normal) : "
<< numNormalZones << endl;
if (numNormalZones > 1)
{
Info<< "More than one normal orientation." << endl;
if (outputThreshold > 0)
{
if (!surfWriter)
{
surfWriter.reset(new surfaceWriters::vtkWriter());
}
writeZoning
(
*surfWriter,
surf,
normalZone,
"normal",
surfFilePath,
surfFileStem
);
if (numNormalZones > outputThreshold)
{
Info<< "Limiting number of files to " << outputThreshold
<< endl;
}
writeParts
(
surf,
min(outputThreshold, numNormalZones),
normalZone,
surfFilePath,
surfFileStem + "_normal"
);
}
}
Info<< endl;
// Check self-intersection
// ~~~~~~~~~~~~~~~~~~~~~~~
if (checkSelfIntersect)
{
Info<< "Checking self-intersection." << endl;
triSurfaceSearch querySurf(surf);
const indexedOctree<treeDataTriSurface>& tree = querySurf.tree();
autoPtr<OBJstream> intStreamPtr;
if (outputThreshold > 0)
{
intStreamPtr.reset(new OBJstream("selfInterPoints.obj"));
}
label nInt = 0;
forAll(surf.edges(), edgei)
{
const edge& e = surf.edges()[edgei];
const point& start = surf.points()[surf.meshPoints()[e[0]]];
const point& end = surf.points()[surf.meshPoints()[e[1]]];
// Exclude hits of connected triangles
treeDataTriSurface::findSelfIntersectOp exclOp(tree, edgei);
pointIndexHit hitInfo(tree.findLineAny(start, end, exclOp));
if (hitInfo.hit())
{
nInt++;
if (intStreamPtr)
{
intStreamPtr().write(hitInfo.hitPoint());
}
// Try and find from other side.
pointIndexHit hitInfo2(tree.findLineAny(end, start, exclOp));
if (hitInfo2.hit() && hitInfo.index() != hitInfo2.index())
{
nInt++;
if (intStreamPtr)
{
intStreamPtr().write(hitInfo2.hitPoint());
}
}
}
}
//// Check very near triangles
//{
// const pointField& localPoints = surf.localPoints();
//
// const boundBox bb(localPoints);
// scalar smallDim = 1e-6 * bb.mag();
// scalar smallDimSqr = Foam::sqr(smallDim);
//
// const pointField& faceCentres = surf.faceCentres();
// forAll(faceCentres, faceI)
// {
// const point& fc = faceCentres[faceI];
// pointIndexHit hitInfo
// (
// tree.findNearest
// (
// fc,
// smallDimSqr,
// findSelfNearOp(tree, faceI)
// )
// );
//
// if (hitInfo.hit() && intStreamPtr)
// {
// intStreamPtr().write(hitInfo.hitPoint());
//
// label nearFaceI = hitInfo.index();
// triPointRef nearTri(surf[nearFaceI].tri(surf.points()));
// triStreamPtr().write
// (
// surf[faceI].tri(surf.points()),
// false
// );
// triStreamPtr().write(nearTri, false);
// nInt++;
// }
// }
//}
if (nInt == 0)
{
Info<< "Surface is not self-intersecting" << endl;
}
else
{
Info<< "Surface is self-intersecting at " << nInt
<< " locations." << endl;
if (intStreamPtr)
{
Info<< "Writing intersection points to "
<< intStreamPtr().name() << endl;
}
}
Info<< endl;
}
Info<< "\nEnd\n" << endl;
return 0;
}
// ************************************************************************* //