diff --git a/CMakeLists.txt b/CMakeLists.txt index a3b3828740..2f9336a52a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -608,6 +608,7 @@ set(SIMPLNX_HDRS ${SIMPLNX_SOURCE_DIR}/Utilities/Meshing/VertexUtilities.hpp ${SIMPLNX_SOURCE_DIR}/Utilities/Meshing/TriangleUtilities.hpp + ${SIMPLNX_SOURCE_DIR}/Utilities/Meshing/TripleLineUtilities.hpp ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/DREAM3D/Dream3dIO.hpp ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/DREAM3D/Dream3dPreflightCache.hpp @@ -807,6 +808,7 @@ set(SIMPLNX_SRCS ${SIMPLNX_SOURCE_DIR}/Utilities/Meshing/VertexUtilities.cpp ${SIMPLNX_SOURCE_DIR}/Utilities/Meshing/TriangleUtilities.cpp + ${SIMPLNX_SOURCE_DIR}/Utilities/Meshing/TripleLineUtilities.cpp ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/DREAM3D/Dream3dIO.cpp ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/DREAM3D/Dream3dPreflightCache.cpp diff --git a/src/Plugins/SimplnxCore/CMakeLists.txt b/src/Plugins/SimplnxCore/CMakeLists.txt index eb8909fa41..ba68a4f889 100644 --- a/src/Plugins/SimplnxCore/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/CMakeLists.txt @@ -105,6 +105,7 @@ set(FilterList PadImageGeometryFilter PartitionGeometryFilter ExtractFeatureBoundaries2DFilter + ExtractTripleLinesFilter PointSampleEdgeGeometryFilter PointSampleTriangleGeometryFilter QuickSurfaceMeshFilter diff --git a/src/Plugins/SimplnxCore/docs/ExtractTripleLinesFilter.md b/src/Plugins/SimplnxCore/docs/ExtractTripleLinesFilter.md new file mode 100644 index 0000000000..5ffd2e0c9e --- /dev/null +++ b/src/Plugins/SimplnxCore/docs/ExtractTripleLinesFilter.md @@ -0,0 +1,74 @@ +# Extract Triple Lines + +## Group (Subgroup) + +Surface Meshing (Generation) + +## Description + +This **Filter** extracts the *triple lines* of a multi-material surface mesh into a new **Edge Geometry**. + +A mesh edge is a triple line segment when the triangles that share it border **three or more distinct +Feature Ids**. Where three grains meet, the segment borders three Feature Ids; where four meet, it +borders four and forms a *quadruple point line*. + +Because the lines are extracted from whatever **Triangle Geometry** is supplied, they always lie exactly +on that surface. This is the reason the extraction is a separate **Filter** rather than an option on the +surface meshing filters: placing it *after* a smoothing step means the triple lines follow the smoothed +surface. + +A typical pipeline is: + +1. **Quick Surface Mesh** (or **SurfaceNets**, or **M3C Surface Meshing**) +2. **Laplacian Smoothing** on the Triangle Geometry +3. **Extract Triple Lines** + +Running the extraction before smoothing instead simply yields the lines of the unsmoothed mesh. + +### Include Exterior Triple Lines + +The outside of the volume is represented in **Face Labels** by the value `-1`. *Include Exterior Triple +Lines* controls whether that counts as a distinct region. + +When disabled (the default), only interior triple lines are produced. When enabled, a grain boundary +that reaches the free surface of the volume also registers as a triple line, because the outside counts +as a third region there. Enabling it therefore produces noticeably more segments. + +### Created Outputs + +The created **Edge Geometry** contains: + +| Output | Description | +|--------|-------------| +| Shared Vertex List | Only the vertices lying on a triple line, copied from the source mesh | +| Shared Edge List | The triple line segments | +| Number of Features | Per segment: `3` for a triple line, `4` for a quadruple point line | +| Node Types | Per vertex, copied from the source mesh's **Node Types** | + +The vertex list is a compacted copy rather than a reference, so the **Edge Geometry** is self-contained: +smoothing or deleting the source **Triangle Geometry** afterwards cannot corrupt it. The trade-off is +that the triple lines will *not* follow the mesh if it is modified after extraction — re-run this +**Filter** to bring them back into alignment. + +**Node Types** is copied through so that the created **Edge Geometry** can be used directly by filters +that require a node type array, such as **Laplacian Smoothing**, which accepts Edge Geometries. + +## Notes + +The ordering of the created vertices and edges is deterministic for a given build but is not guaranteed +to be identical across platforms. Comparisons against a stored exemplar should sort the edges and +vertices first rather than relying on their order. + +% Auto generated parameter table will be inserted here + +## Example Pipelines + +## License & Copyright + +Please see the description file distributed with this **Plugin** + +## DREAM3D-NX Help + +If you need help, need to file a bug report or want to request a new feature, please head over to the +[DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues/discussions) GitHub site where +the community of DREAM3D-NX users can help answer your questions. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/QuickSurfaceMesh.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/QuickSurfaceMesh.cpp index c4f717cd56..04c695af55 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/QuickSurfaceMesh.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/QuickSurfaceMesh.cpp @@ -2,15 +2,10 @@ #include "TupleTransfer.hpp" #include "simplnx/DataStructure/DataArray.hpp" -#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/Meshing/TriangleUtilities.hpp" -#include "simplnx/Utilities/ParallelData3DAlgorithm.hpp" - -#include -#include using namespace nx::core; @@ -24,206 +19,6 @@ std::mt19937_64 k_Generator(k_RandomDevice()); // Standard mersenne_twister_engi std::mt19937_64::result_type k_Seed = 3412341234123412; std::uniform_real_distribution<> k_Distribution(k_RangeMin, k_RangeMax); -template -void hashCombine(size_t& seed, const T& obj) -{ - std::hash hasher; - seed ^= hasher(obj) + 0x9e3779b9 + (seed << 6) + (seed >> 2); -} - -// ----------------------------------------------------------------------------- -using VertexType = std::array; -using EdgeType = std::array; - -// ----------------------------------------------------------------------------- -struct VertexHasher -{ - size_t operator()(const VertexType& vert) const - { - size_t hash = std::hash()(vert[0]); - hashCombine(hash, vert[1]); - hashCombine(hash, vert[2]); - return hash; - } -}; - -// ----------------------------------------------------------------------------- -struct EdgeHasher -{ - size_t operator()(const EdgeType& edge) const - { - size_t hash = std::hash()(edge[0]); - hashCombine(hash, edge[1]); - return hash; - } -}; - -// ----------------------------------------------------------------------------- -using VertexMap = std::unordered_map; -using EdgeMap = std::unordered_map; - -// ----------------------------------------------------------------------------- -struct GenerateTripleLinesImpl -{ - using MeshIndexType = typename QuickSurfaceMesh::MeshIndexType; - - GenerateTripleLinesImpl(ImageGeom* imageGeom, Int32AbstractDataStore& featureIdsStore, VertexMap& vertexMapRef, EdgeMap& edgeMapRef) - : origin(imageGeom->getOrigin()) - , res(imageGeom->getSpacing()) - , featureIds(featureIdsStore) - , vertexMap(vertexMapRef) - , edgeMap(edgeMapRef) - { - SizeVec3 udims = imageGeom->getDimensions(); - - xP = udims[0]; - yP = udims[1]; - zP = udims[2]; - } - - void compute(usize zStart, usize zEnd, usize yStart, usize yEnd, usize xStart, usize xEnd) const - { - for(size_t k = zStart; k < zEnd; k++) - { - for(size_t j = yStart; j < yEnd; j++) - { - for(size_t i = xStart; i < xEnd; i++) - { - point = (k * xP * yP) + (j * xP) + i; - // Case 1 - neigh1 = point + 1; - neigh2 = point + (xP * yP) + 1; - neigh3 = point + (xP * yP); - - VertexType const p0 = {{origin[0] + static_cast(i) * res[0] + res[0], origin[1] + static_cast(j) * res[1] + res[1], origin[2] + static_cast(k) * res[2] + res[2]}}; - - VertexType const p1 = {{origin[0] + static_cast(i) * res[0] + res[0], origin[1] + static_cast(j) * res[1], origin[2] + static_cast(k) * res[2] + res[2]}}; - - VertexType const p2 = {{origin[0] + static_cast(i) * res[0], origin[1] + static_cast(j) * res[1] + res[1], origin[2] + static_cast(k) * res[2] + res[2]}}; - - VertexType const p3 = {{origin[0] + static_cast(i) * res[0] + res[0], origin[1] + static_cast(j) * res[1] + res[1], origin[2] + static_cast(k) * res[2]}}; - - uFeatures.clear(); - uFeatures.insert(featureIds[point]); - uFeatures.insert(featureIds[neigh1]); - uFeatures.insert(featureIds[neigh2]); - uFeatures.insert(featureIds[neigh3]); - - if(uFeatures.size() > 2) - { - auto iter = vertexMap.find(p0); - if(iter == vertexMap.end()) - { - vertexMap[p0] = vertCounter++; - } - iter = vertexMap.find(p1); - if(iter == vertexMap.end()) - { - vertexMap[p1] = vertCounter++; - } - MeshIndexType i0 = vertexMap[p0]; - MeshIndexType i1 = vertexMap[p1]; - - const EdgeType tmpEdge = {{i0, i1}}; - auto eiter = edgeMap.find(tmpEdge); - if(eiter == edgeMap.end()) - { - edgeMap[tmpEdge] = edgeCounter++; - } - } - - // Case 2 - neigh1 = point + xP; - neigh2 = point + (xP * yP) + xP; - neigh3 = point + (xP * yP); - - uFeatures.clear(); - uFeatures.insert(featureIds[point]); - uFeatures.insert(featureIds[neigh1]); - uFeatures.insert(featureIds[neigh2]); - uFeatures.insert(featureIds[neigh3]); - if(uFeatures.size() > 2) - { - auto iter = vertexMap.find(p0); - if(iter == vertexMap.end()) - { - vertexMap[p0] = vertCounter++; - } - iter = vertexMap.find(p2); - if(iter == vertexMap.end()) - { - vertexMap[p2] = vertCounter++; - } - - MeshIndexType i0 = vertexMap[p0]; - MeshIndexType i2 = vertexMap[p2]; - - const EdgeType tmpEdge = {{i0, i2}}; - auto eiter = edgeMap.find(tmpEdge); - if(eiter == edgeMap.end()) - { - edgeMap[tmpEdge] = edgeCounter++; - } - } - - // Case 3 - neigh1 = point + 1; - neigh2 = point + xP + 1; - neigh3 = point + +xP; - - uFeatures.clear(); - uFeatures.insert(featureIds[point]); - uFeatures.insert(featureIds[neigh1]); - uFeatures.insert(featureIds[neigh2]); - uFeatures.insert(featureIds[neigh3]); - if(uFeatures.size() > 2) - { - auto iter = vertexMap.find(p0); - if(iter == vertexMap.end()) - { - vertexMap[p0] = vertCounter++; - } - iter = vertexMap.find(p3); - if(iter == vertexMap.end()) - { - vertexMap[p3] = vertCounter++; - } - - MeshIndexType i0 = vertexMap[p0]; - MeshIndexType i3 = vertexMap[p3]; - - const EdgeType tmpEdge = {{i0, i3}}; - auto eiter = edgeMap.find(tmpEdge); - if(eiter == edgeMap.end()) - { - edgeMap[tmpEdge] = edgeCounter++; - } - } - } - } - } - } - - void operator()(const Range3D& range) const - { - compute(range[0], range[1], range[2], range[3], range[4], range[5]); - } - -private: - mutable MeshIndexType point = 0, neigh1 = 0, neigh2 = 0, neigh3 = 0; - mutable MeshIndexType vertCounter = 0; - mutable MeshIndexType edgeCounter = 0; - mutable MeshIndexType xP; - mutable MeshIndexType yP; - mutable MeshIndexType zP; - FloatVec3 origin; - FloatVec3 res; - Int32AbstractDataStore& featureIds; - VertexMap& vertexMap; - EdgeMap& edgeMap; - mutable std::set uFeatures; -}; - // ----------------------------------------------------------------------------- void GetGridCoordinates(const IGridGeometry* grid, size_t x, size_t y, size_t z, QuickSurfaceMesh::VertexStore& verts, IGeometry::MeshIndexType nodeIndex) { @@ -435,76 +230,6 @@ Result<> QuickSurfaceMesh::operator()() m_DataStructure.removeData(triangleGeom.getElementNeighborsId().value()); } -#ifdef QSM_CREATE_TRIPLE_LINES - if(m_InputValues->pGenerateTripleLines) - { - IGeometry::SharedTriList* triangle = triangleGeom.getFaces(); - IGeometry::SharedVertexList* vertices = triangleGeom.getVertices(); - - EdgeGeom* edgeGeom = EdgeGeom::Create(m_DataStructure, "[EdgeType Geometry]", parentGroupId); - edgeGeom->setVertices(*vertices); - - Int32Array& nodeTypes = m_DataStructure.getDataRefAs(m_InputValues->pFaceLabelsDataPath); - - MeshIndexType edgeCount = 0; - for(MeshIndexType i = 0; i < triangleCount; i++) - { - MeshIndexType n1 = (*triangle)[3 * i + 0]; - MeshIndexType n2 = (*triangle)[3 * i + 1]; - MeshIndexType n3 = (*triangle)[3 * i + 2]; - if(nodeTypes[n1] >= 3 && nodeTypes[n2] >= 3) - { - edgeCount++; - } - if(nodeTypes[n1] >= 3 && nodeTypes[n3] >= 3) - { - edgeCount++; - } - if(nodeTypes[n2] >= 3 && nodeTypes[n3] >= 3) - { - edgeCount++; - } - } - - std::string edgeGeometryName = "[EdgeType Geometry]"; - DataPath edgeGeometryDataPath = m_InputValues->pParentDataGroupPath.createChildPath(edgeGeometryName); - std::string sharedEdgeListName = "SharedEdgeList"; - size_t numEdges = edgeCount; - size_t numEdgeComps = 2; - IGeometry::SharedEdgeList* edges = - IGeometry::SharedEdgeList::CreateWithStore>(m_DataStructure, sharedEdgeListName, {numEdges}, {numEdgeComps}, m_DataStructure.getId(edgeGeometryDataPath)); - - edgeCount = 0; - for(MeshIndexType i = 0; i < triangleCount; i++) - { - MeshIndexType n1 = (*triangle)[3 * i + 0]; - MeshIndexType n2 = (*triangle)[3 * i + 1]; - MeshIndexType n3 = (*triangle)[3 * i + 2]; - if(nodeTypes[n1] >= 3 && nodeTypes[n2] >= 3) - { - (*edges)[2 * edgeCount] = n1; - (*edges)[2 * edgeCount + 1] = n2; - edgeCount++; - } - if(nodeTypes[n1] >= 3 && nodeTypes[n3] >= 3) - { - (*edges)[2 * edgeCount] = n1; - (*edges)[2 * edgeCount + 1] = n3; - edgeCount++; - } - if(nodeTypes[n2] >= 3 && nodeTypes[n3] >= 3) - { - (*edges)[2 * edgeCount] = n2; - (*edges)[2 * edgeCount + 1] = n3; - edgeCount++; - } - } - - // Now that we all of that out of the way, generate the triple lines - generateTripleLines(); - } -#endif - // Guarded on windingResult still being valid so a genuine winding-repair error is never discarded. if(m_InputValues->BoundingBoxSkinMode == BoundingBoxSkinMode::k_BackgroundBackedWallsOnly && windingResult.valid()) { @@ -1659,231 +1384,3 @@ void QuickSurfaceMesh::createNodesAndTriangles(std::vector& m_Nod } } } - -// ----------------------------------------------------------------------------- -void QuickSurfaceMesh::generateTripleLines() -{ - if(m_ShouldCancel) - { - return; - } - /** - * This is a bit of experimental code where we define a triple line as an edge - * that shares voxels with at least 3 unique Feature Ids. This is different - * than saying that an edge is part of a triple line if it's nodes are considered - * sharing at least 3 unique voxels. This code is not complete as it will only - * find "interior" triple lines and no lines on the surface. I am going to leave - * this bit of code in place for historical reasons so that we can refer to it - * later if needed. - * Mike Jackson, JULY 2018 - */ - m_MessageHandler(IFilter::Message::Type::Info, "Generating Triple Lines"); - - Int32AbstractDataStore& featureIds = m_DataStructure.getDataAs(m_InputValues->FeatureIdsArrayPath)->getDataStoreRef(); - - int32_t numFeatures = 0; - size_t numTuples = featureIds.getNumberOfTuples(); - for(size_t i = 0; i < numTuples; i++) - { - if(featureIds[i] > numFeatures) - { - numFeatures = featureIds[i]; - } - } - - std::vector featDims = {static_cast(numFeatures) + 1}; - - auto* imageGeom = m_DataStructure.getDataAs(m_InputValues->GridGeomDataPath); - - SizeVec3 udims = imageGeom->getDimensions(); - - MeshIndexType xP = udims[0]; - MeshIndexType yP = udims[1]; - MeshIndexType zP = udims[2]; - MeshIndexType point = 0, neigh1 = 0, neigh2 = 0, neigh3 = 0; - - std::set uFeatures; - - FloatVec3 origin = imageGeom->getOrigin(); - FloatVec3 res = imageGeom->getSpacing(); - - VertexMap vertexMap; - EdgeMap edgeMap; - MeshIndexType vertCounter = 0; - MeshIndexType edgeCounter = 0; - - // Cycle through again assigning coordinates to each node and assigning node numbers and feature labels to each triangle - ParallelData3DAlgorithm algorithm; - algorithm.setRange(Range3D(xP - 1, yP - 1, zP - 1)); - if(featureIds.getChunkShape().has_value()) - { - const auto chunkShape = featureIds.getChunkShape().value(); - algorithm.setChunkSize(Range3D(chunkShape[0], chunkShape[1], chunkShape[2])); - } - algorithm.setParallelizationEnabled(false); - algorithm.execute(GenerateTripleLinesImpl(imageGeom, featureIds, vertexMap, edgeMap)); - -#if QSM_CREATE_TRIPLE_LINES - for(size_t k = 0; k < zP - 1; k++) - { - for(size_t j = 0; j < yP - 1; j++) - { - for(size_t i = 0; i < xP - 1; i++) - { - point = (k * xP * yP) + (j * xP) + i; - // Case 1 - neigh1 = point + 1; - neigh2 = point + (xP * yP) + 1; - neigh3 = point + (xP * yP); - - VertexType p0 = {{origin[0] + static_cast(i) * res[0] + res[0], origin[1] + static_cast(j) * res[1] + res[1], origin[2] + static_cast(k) * res[2] + res[2]}}; - - VertexType p1 = {{origin[0] + static_cast(i) * res[0] + res[0], origin[1] + static_cast(j) * res[1], origin[2] + static_cast(k) * res[2] + res[2]}}; - - VertexType p2 = {{origin[0] + static_cast(i) * res[0], origin[1] + static_cast(j) * res[1] + res[1], origin[2] + static_cast(k) * res[2] + res[2]}}; - - VertexType p3 = {{origin[0] + static_cast(i) * res[0] + res[0], origin[1] + static_cast(j) * res[1] + res[1], origin[2] + static_cast(k) * res[2]}}; - - uFeatures.clear(); - uFeatures.insert(featureIds[point]); - uFeatures.insert(featureIds[neigh1]); - uFeatures.insert(featureIds[neigh2]); - uFeatures.insert(featureIds[neigh3]); - - if(uFeatures.size() > 2) - { - auto iter = vertexMap.find(p0); - if(iter == vertexMap.end()) - { - vertexMap[p0] = vertCounter++; - } - iter = vertexMap.find(p1); - if(iter == vertexMap.end()) - { - vertexMap[p1] = vertCounter++; - } - MeshIndexType i0 = vertexMap[p0]; - MeshIndexType i1 = vertexMap[p1]; - - EdgeType tmpEdge = {{i0, i1}}; - auto eiter = edgeMap.find(tmpEdge); - if(eiter == edgeMap.end()) - { - edgeMap[tmpEdge] = edgeCounter++; - } - } - - // Case 2 - neigh1 = point + xP; - neigh2 = point + (xP * yP) + xP; - neigh3 = point + (xP * yP); - - uFeatures.clear(); - uFeatures.insert(featureIds[point]); - uFeatures.insert(featureIds[neigh1]); - uFeatures.insert(featureIds[neigh2]); - uFeatures.insert(featureIds[neigh3]); - if(uFeatures.size() > 2) - { - auto iter = vertexMap.find(p0); - if(iter == vertexMap.end()) - { - vertexMap[p0] = vertCounter++; - } - iter = vertexMap.find(p2); - if(iter == vertexMap.end()) - { - vertexMap[p2] = vertCounter++; - } - - MeshIndexType i0 = vertexMap[p0]; - MeshIndexType i2 = vertexMap[p2]; - - EdgeType tmpEdge = {{i0, i2}}; - auto eiter = edgeMap.find(tmpEdge); - if(eiter == edgeMap.end()) - { - edgeMap[tmpEdge] = edgeCounter++; - } - } - - // Case 3 - neigh1 = point + 1; - neigh2 = point + xP + 1; - neigh3 = point + +xP; - - uFeatures.clear(); - uFeatures.insert(featureIds[point]); - uFeatures.insert(featureIds[neigh1]); - uFeatures.insert(featureIds[neigh2]); - uFeatures.insert(featureIds[neigh3]); - if(uFeatures.size() > 2) - { - auto iter = vertexMap.find(p0); - if(iter == vertexMap.end()) - { - vertexMap[p0] = vertCounter++; - } - iter = vertexMap.find(p3); - if(iter == vertexMap.end()) - { - vertexMap[p3] = vertCounter++; - } - - MeshIndexType i0 = vertexMap[p0]; - MeshIndexType i3 = vertexMap[p3]; - - EdgeType tmpEdge = {{i0, i3}}; - auto eiter = edgeMap.find(tmpEdge); - if(eiter == edgeMap.end()) - { - edgeMap[tmpEdge] = edgeCounter++; - } - } - } - } - } -#endif - - std::string edgeGeometryName = "[Edge Geometry]"; - - DataPath edgeGeometryDataPath({edgeGeometryName}); - std::string sharedVertListName = "SharedVertList"; - DataPath sharedVertListDataPath = edgeGeometryDataPath.createChildPath(sharedVertListName); - - EdgeGeom* tripleLineEdge = EdgeGeom::Create(m_DataStructure, edgeGeometryName); - size_t numVerts = vertexMap.size(); - size_t numComps = 3; - IGeometry::SharedVertexList* vertices = Float32Array::CreateWithStore>(m_DataStructure, sharedVertListName, {numVerts}, {numComps}, m_DataStructure.getId(edgeGeometryDataPath)); - auto& verticesRef = vertices->getDataStoreRef(); - - for(const auto& vert : vertexMap) - { - float v0 = vert.first[0]; - float v1 = vert.first[1]; - float v2 = vert.first[2]; - MeshIndexType idx = vert.second; - const MeshIndexType offset = idx * numComps; - verticesRef.setValue(offset + 0, v0); - verticesRef.setValue(+1, v1); - verticesRef.setValue(idx * numComps + 2, v2); - } - tripleLineEdge->setVertices(*vertices); - - std::string sharedEdgeListName = "SharedEdgeList"; - size_t numEdges = edgeMap.size(); - size_t numEdgeComps = 2; - IGeometry::SharedEdgeList* edges = - IGeometry::SharedEdgeList::CreateWithStore>(m_DataStructure, sharedEdgeListName, {numEdges}, {numEdgeComps}, m_DataStructure.getId(edgeGeometryDataPath)); - auto& edgesRef = edges->getDataStoreRef(); - - for(const auto& edge : edgeMap) - { - MeshIndexType i0 = edge.first[0]; - MeshIndexType i1 = edge.first[1]; - MeshIndexType idx = edge.second; - edgesRef.setValue(idx * numComps + 0, i0); - edgesRef.setValue(idx * numComps + 1, i1); - } - tripleLineEdge->setEdgeList(*edges); -} diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/QuickSurfaceMesh.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/QuickSurfaceMesh.hpp index f46a98cda2..84fc243809 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/QuickSurfaceMesh.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/QuickSurfaceMesh.hpp @@ -19,7 +19,6 @@ struct SIMPLNXCORE_EXPORT QuickSurfaceMeshInputValues { bool FixProblemVoxels; bool RepairTriangleWinding; - bool GenerateTripleLines; ChoicesParameter::ValueType BoundingBoxSkinMode; DataPath GridGeomDataPath; @@ -75,16 +74,10 @@ class SIMPLNXCORE_EXPORT QuickSurfaceMesh */ void createNodesAndTriangles(std::vector& m_NodeIds, MeshIndexType nodeCount, MeshIndexType triangleCount); - /** - * @brief generateTripleLines - */ - void generateTripleLines(); - private: DataStructure& m_DataStructure; const QuickSurfaceMeshInputValues* m_InputValues = nullptr; const std::atomic_bool& m_ShouldCancel; const IFilter::MessageHandler& m_MessageHandler; - bool m_GenerateTripleLines = false; }; } // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractTripleLinesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractTripleLinesFilter.cpp new file mode 100644 index 0000000000..8cab428ef1 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractTripleLinesFilter.cpp @@ -0,0 +1,174 @@ +#include "ExtractTripleLinesFilter.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" +#include "simplnx/Filter/Actions/CreateArrayAction.hpp" +#include "simplnx/Filter/Actions/CreateGeometry1DAction.hpp" +#include "simplnx/Parameters/ArraySelectionParameter.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/DataGroupCreationParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/Meshing/TripleLineUtilities.hpp" + +using namespace nx::core; + +namespace nx::core +{ +//------------------------------------------------------------------------------ +std::string ExtractTripleLinesFilter::name() const +{ + return FilterTraits::name.str(); +} + +//------------------------------------------------------------------------------ +std::string ExtractTripleLinesFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid ExtractTripleLinesFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string ExtractTripleLinesFilter::humanName() const +{ + return "Extract Triple Lines"; +} + +//------------------------------------------------------------------------------ +std::vector ExtractTripleLinesFilter::defaultTags() const +{ + return {className(), "Surface Meshing", "Triple Lines", "Edge Geometry", "Generation"}; +} + +//------------------------------------------------------------------------------ +Parameters ExtractTripleLinesFilter::parameters() const +{ + Parameters params; + + params.insertSeparator(Parameters::Separator{"Input Parameters"}); + params.insert(std::make_unique(k_IncludeExteriorTripleLines_Key, "Include Exterior Triple Lines", + "If true, the outside of the volume counts as a distinct region, so grain boundaries reaching the free surface register as triple lines", false)); + + params.insertSeparator(Parameters::Separator{"Required Input Triangle Geometry"}); + params.insert(std::make_unique(k_TriangleGeometryPath_Key, "Triangle Geometry", "The surface mesh from which to extract the triple lines", DataPath{}, + GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Triangle})); + params.insert(std::make_unique(k_FaceLabelsArrayPath_Key, "Face Labels", "The Array specifying which Features are on either side of each Face in the Triangle Geometry", + DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{2}})); + params.insert(std::make_unique(k_NodeTypesArrayPath_Key, "Node Types", "The Array specifying the type of node in the Triangle Geometry. Copied onto the created vertices", + DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int8}, ArraySelectionParameter::AllowedComponentShapes{{1}})); + + params.insertSeparator(Parameters::Separator{"Output Triple Line Geometry"}); + params.insert(std::make_unique(k_CreatedTripleLineGeometryPath_Key, "Created Triple Line Geometry", "The name of the created Triple Line Edge Geometry", + DataPath({"Triple Lines"}))); + params.insert(std::make_unique(k_VertexDataGroupName_Key, "Vertex Data", "The name of the Attribute Matrix holding the Vertex Data of the Triple Line Geometry", + INodeGeometry0D::k_VertexAttributeMatrixName)); + params.insert(std::make_unique(k_EdgeDataGroupName_Key, "Edge Data", "The name of the Attribute Matrix holding the Edge Data of the Triple Line Geometry", + INodeGeometry1D::k_EdgeAttributeMatrixName)); + params.insert(std::make_unique(k_NumFeaturesArrayName_Key, "Number of Features", + "The name of the Array holding the number of unique Feature Ids bordering each triple line segment (3 or 4)", "NumFeatures")); + params.insert(std::make_unique(k_NodeTypesArrayName_Key, "Node Types", "The name of the created Array holding the Node Type of each triple line vertex", "NodeTypes")); + + return params; +} + +//------------------------------------------------------------------------------ +IFilter::VersionType ExtractTripleLinesFilter::parametersVersion() const +{ + return 1; +} + +//------------------------------------------------------------------------------ +IFilter::UniquePointer ExtractTripleLinesFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult ExtractTripleLinesFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + auto pTriangleGeometryPath = filterArgs.value(k_TriangleGeometryPath_Key); + auto pFaceLabelsArrayPath = filterArgs.value(k_FaceLabelsArrayPath_Key); + auto pNodeTypesArrayPath = filterArgs.value(k_NodeTypesArrayPath_Key); + auto pTripleLineGeometryPath = filterArgs.value(k_CreatedTripleLineGeometryPath_Key); + auto pVertexDataName = filterArgs.value(k_VertexDataGroupName_Key); + auto pEdgeDataName = filterArgs.value(k_EdgeDataGroupName_Key); + auto pNumFeaturesName = filterArgs.value(k_NumFeaturesArrayName_Key); + auto pNodeTypesName = filterArgs.value(k_NodeTypesArrayName_Key); + + nx::core::Result resultOutputActions; + std::vector preflightUpdatedValues; + + const auto& triangleGeom = dataStructure.getDataRefAs(pTriangleGeometryPath); + const auto& faceLabels = dataStructure.getDataRefAs(pFaceLabelsArrayPath); + const auto& nodeTypes = dataStructure.getDataRefAs(pNodeTypesArrayPath); + + // The selection parameters guarantee these arrays exist with the right type and component shape, + // but not that they are sized to this particular geometry. Executing with a mismatch would read + // past the end of one of them, so check here where the user still gets an actionable message. + if(faceLabels.getNumberOfTuples() != triangleGeom.getNumberOfFaces()) + { + return {MakeErrorResult(-57200, fmt::format("Face Labels array '{}' has {} tuples but Triangle Geometry '{}' has {} faces. They must match.", pFaceLabelsArrayPath.toString(), + faceLabels.getNumberOfTuples(), pTriangleGeometryPath.toString(), triangleGeom.getNumberOfFaces()))}; + } + if(nodeTypes.getNumberOfTuples() != triangleGeom.getNumberOfVertices()) + { + return {MakeErrorResult(-57201, fmt::format("Node Types array '{}' has {} tuples but Triangle Geometry '{}' has {} vertices. They must match.", pNodeTypesArrayPath.toString(), + nodeTypes.getNumberOfTuples(), pTriangleGeometryPath.toString(), triangleGeom.getNumberOfVertices()))}; + } + + const std::string dataStoreFormat = faceLabels.getDataFormat(); + + // The edge and vertex counts are not known until the mesh has actually been scanned, so the + // geometry is created empty and resized during execute, as the surface meshers do. + { + auto createGeometryAction = + std::make_unique(pTripleLineGeometryPath, 0, 0, pVertexDataName, pEdgeDataName, EdgeGeom::k_SharedVertexListName, EdgeGeom::k_SharedEdgeListName); + resultOutputActions.value().appendAction(std::move(createGeometryAction)); + } + { + auto createNumFeaturesAction = std::make_unique(nx::core::DataType::int8, std::vector{0}, std::vector{1}, + pTripleLineGeometryPath.createChildPath(pEdgeDataName).createChildPath(pNumFeaturesName), dataStoreFormat); + resultOutputActions.value().appendAction(std::move(createNumFeaturesAction)); + } + { + auto createNodeTypesAction = std::make_unique(nx::core::DataType::int8, std::vector{0}, std::vector{1}, + pTripleLineGeometryPath.createChildPath(pVertexDataName).createChildPath(pNodeTypesName), dataStoreFormat); + resultOutputActions.value().appendAction(std::move(createNodeTypesAction)); + } + + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} + +//------------------------------------------------------------------------------ +Result<> ExtractTripleLinesFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + auto pTripleLineGeometryPath = filterArgs.value(k_CreatedTripleLineGeometryPath_Key); + auto pVertexDataName = filterArgs.value(k_VertexDataGroupName_Key); + auto pEdgeDataName = filterArgs.value(k_EdgeDataGroupName_Key); + + const auto& triangleGeom = dataStructure.getDataRefAs(filterArgs.value(k_TriangleGeometryPath_Key)); + const auto& faceLabelsRef = dataStructure.getDataRefAs(filterArgs.value(k_FaceLabelsArrayPath_Key)).getDataStoreRef(); + const auto& sourceNodeTypesRef = dataStructure.getDataRefAs(filterArgs.value(k_NodeTypesArrayPath_Key)).getDataStoreRef(); + + auto& tripleLineGeom = dataStructure.getDataRefAs(pTripleLineGeometryPath); + auto& numFeaturesRef = + dataStructure.getDataRefAs(pTripleLineGeometryPath.createChildPath(pEdgeDataName).createChildPath(filterArgs.value(k_NumFeaturesArrayName_Key))).getDataStoreRef(); + auto& tripleLineNodeTypesRef = + dataStructure.getDataRefAs(pTripleLineGeometryPath.createChildPath(pVertexDataName).createChildPath(filterArgs.value(k_NodeTypesArrayName_Key))).getDataStoreRef(); + + MeshingUtilities::TripleLineOptions tripleLineOptions; + tripleLineOptions.IncludeExteriorLines = filterArgs.value(k_IncludeExteriorTripleLines_Key); + + return MeshingUtilities::GenerateTripleLines(triangleGeom, faceLabelsRef, sourceNodeTypesRef, tripleLineGeom, numFeaturesRef, tripleLineNodeTypesRef, tripleLineOptions, shouldCancel, + messageHandler); +} +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractTripleLinesFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractTripleLinesFilter.hpp new file mode 100644 index 0000000000..330a7d2a4e --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractTripleLinesFilter.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/Filter/FilterTraits.hpp" +#include "simplnx/Filter/IFilter.hpp" + +namespace nx::core +{ +/** + * @class ExtractTripleLinesFilter + * @brief Extracts the triple lines of a multi-material surface mesh into an Edge Geometry. + * + * A mesh edge is a triple line segment when the triangles sharing it border three or more + * distinct Feature Ids. Because the lines are extracted from whatever mesh is supplied, placing + * this filter after a smoothing filter yields triple lines that lie exactly on the smoothed + * surface. + */ +class SIMPLNXCORE_EXPORT ExtractTripleLinesFilter : public IFilter +{ +public: + ExtractTripleLinesFilter() = default; + ~ExtractTripleLinesFilter() noexcept override = default; + + ExtractTripleLinesFilter(const ExtractTripleLinesFilter&) = delete; + ExtractTripleLinesFilter(ExtractTripleLinesFilter&&) noexcept = delete; + + ExtractTripleLinesFilter& operator=(const ExtractTripleLinesFilter&) = delete; + ExtractTripleLinesFilter& operator=(ExtractTripleLinesFilter&&) noexcept = delete; + + // Parameter Keys + static constexpr StringLiteral k_TriangleGeometryPath_Key = "input_triangle_geometry_path"; + static constexpr StringLiteral k_FaceLabelsArrayPath_Key = "face_labels_array_path"; + static constexpr StringLiteral k_NodeTypesArrayPath_Key = "node_types_array_path"; + static constexpr StringLiteral k_IncludeExteriorTripleLines_Key = "include_exterior_triple_lines"; + + static constexpr StringLiteral k_CreatedTripleLineGeometryPath_Key = "output_triple_line_geometry_path"; + static constexpr StringLiteral k_VertexDataGroupName_Key = "vertex_data_group_name"; + static constexpr StringLiteral k_EdgeDataGroupName_Key = "edge_data_group_name"; + static constexpr StringLiteral k_NumFeaturesArrayName_Key = "num_features_array_name"; + static constexpr StringLiteral k_NodeTypesArrayName_Key = "node_types_array_name"; + + /** + * @brief Returns the name of the filter. + * @return + */ + std::string name() const override; + + /** + * @brief Returns the C++ classname of this filter. + * @return + */ + std::string className() const override; + + /** + * @brief Returns the uuid of the filter. + * @return + */ + Uuid uuid() const override; + + /** + * @brief Returns the human readable name of the filter. + * @return + */ + std::string humanName() const override; + + /** + * @brief Returns the default tags for this filter. + * @return + */ + std::vector defaultTags() const override; + + /** + * @brief Returns the parameters of the filter (i.e. its inputs) + * @return + */ + Parameters parameters() const override; + + /** + * @brief Returns parameters version integer. + * Initial version should always be 1. + * Should be incremented everytime the parameters change. + * @return VersionType + */ + VersionType parametersVersion() const override; + + /** + * @brief Returns a copy of the filter. + * @return + */ + UniquePointer clone() const override; + +protected: + /** + * @brief Takes in a DataStructure and checks that the filter can be run on it with the given arguments. + * Returns any warnings/errors. Also returns the changes that would be applied to the DataStructure. + * Some parts of the actions may not be completely filled out if all the required information is not available at preflight time. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; + + /** + * @brief Applies the filter's algorithm to the DataStructure with the given arguments. Returns any warnings/errors. + * On failure, there is no guarantee that the DataStructure is in a correct state. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param pipelineNode The node in the pipeline that is being executed + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; +}; +} // namespace nx::core + +SIMPLNX_DEF_FILTER_TRAITS(nx::core, ExtractTripleLinesFilter, "1abc8078-6997-4455-b908-e1cdb890da77"); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/QuickSurfaceMeshFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/QuickSurfaceMeshFilter.cpp index 5542a3d6ae..6f417f7418 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/QuickSurfaceMeshFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/QuickSurfaceMeshFilter.cpp @@ -70,7 +70,6 @@ Parameters QuickSurfaceMeshFilter::parameters() const "background (Feature Id 0); faces where the wall caps a real Feature ARE still generated, so " "Features flush with the box stay closed.", BoundingBoxSkinMode::k_Off, ChoicesParameter::Choices{"Off", "Background-Backed Walls Only"})); - // params.insert(std::make_unique(k_GenerateTripleLines_Key, "Generate Triple Lines", "Experimental feature. May not work.", false)); params.insert(std::make_unique(k_GridGeometryDataPath_Key, "Grid Geometry", "The complete path to the Grid Geometry from which to create a Triangle Geometry", DataPath{}, GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image, IGeometry::Type::RectGrid})); @@ -109,16 +108,16 @@ Parameters QuickSurfaceMeshFilter::parameters() const IFilter::VersionType QuickSurfaceMeshFilter::parametersVersion() const { return 3; - // Version 2 -> 3 - // Change 1: - // Added - k_BoundingBoxSkinMode_Key = "bounding_box_skin_mode_index"; - // Solution - set the value to 0 (BoundingBoxSkinMode::k_Off, preserves prior behavior); - // // Version 1 -> 2 // Change 1: // Added - k_RepairTriangleWinding_Key = "repair_triangle_winding"; // Solution - set the value to false (not default); // + // Version 2 -> 3 + // Change 1: + // Added - k_BoundingBoxSkinMode_Key = "bounding_box_skin_mode_index"; + // Solution - set the value to 0 (BoundingBoxSkinMode::k_Off, preserves prior behavior); + // } //------------------------------------------------------------------------------ @@ -221,7 +220,6 @@ Result<> QuickSurfaceMeshFilter::executeImpl(DataStructure& dataStructure, const { nx::core::QuickSurfaceMeshInputValues inputValues; - // inputValues.GenerateTripleLines = filterArgs.value(k_GenerateTripleLines_Key); inputValues.FixProblemVoxels = filterArgs.value(k_FixProblemVoxels_Key); inputValues.RepairTriangleWinding = filterArgs.value(k_RepairTriangleWinding_Key); inputValues.BoundingBoxSkinMode = filterArgs.value(k_BoundingBoxSkinMode_Key); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/QuickSurfaceMeshFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/QuickSurfaceMeshFilter.hpp index 91c8b0672c..73bbe67caf 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/QuickSurfaceMeshFilter.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/QuickSurfaceMeshFilter.hpp @@ -25,7 +25,6 @@ class SIMPLNXCORE_EXPORT QuickSurfaceMeshFilter : public IFilter // Parameter Keys - // static constexpr StringLiteral k_GenerateTripleLines_Key = "generate_triple_lines"; static constexpr StringLiteral k_RepairTriangleWinding_Key = "repair_triangle_winding"; static constexpr StringLiteral k_FixProblemVoxels_Key = "fix_problem_voxels"; static constexpr StringLiteral k_BoundingBoxSkinMode_Key = "bounding_box_skin_mode_index"; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/SurfaceNetsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/SurfaceNetsFilter.cpp index e504f84408..511ac2d400 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/SurfaceNetsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/SurfaceNetsFilter.cpp @@ -105,11 +105,6 @@ Parameters SurfaceNetsFilter::parameters() const params.insert(std::make_unique(k_FaceLabelsArrayName_Key, "Face Labels", "The complete path to the Array specifying which Features are on either side of each Face in the Triangle Geometry", "FaceLabels")); - // Associate the Linkable Parameter(s) to the children parameters that they control - params.linkParameters(k_ApplySmoothing_Key, k_SmoothingIterations_Key, true); - params.linkParameters(k_ApplySmoothing_Key, k_MaxDistanceFromVoxelCenter_Key, true); - params.linkParameters(k_ApplySmoothing_Key, k_RelaxationFactor_Key, true); - return params; } @@ -203,6 +198,7 @@ IFilter::PreflightResult SurfaceNetsFilter::preflightImpl(const DataStructure& d resultOutputActions.value().appendAction(std::move(createArrayAction)); } } + return {std::move(resultOutputActions)}; } diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index 49d8eb54d3..a67ba3e9c5 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -100,6 +100,7 @@ set(${PLUGIN_NAME}UnitTest_SRCS KeepRemoveRankedFeaturesTest.cpp LabelTriangleGeometryTest.cpp LaplacianSmoothingFilterTest.cpp + ExtractTripleLinesTest.cpp M3CSurfaceMeshingTest.cpp MapPointCloudToRegularGridTest.cpp MoveDataTest.cpp diff --git a/src/Plugins/SimplnxCore/test/ExtractTripleLinesTest.cpp b/src/Plugins/SimplnxCore/test/ExtractTripleLinesTest.cpp new file mode 100644 index 0000000000..0b021456e2 --- /dev/null +++ b/src/Plugins/SimplnxCore/test/ExtractTripleLinesTest.cpp @@ -0,0 +1,381 @@ +#include "SimplnxCore/Filters/ExtractTripleLinesFilter.hpp" +#include "SimplnxCore/Filters/M3CSurfaceMeshingFilter.hpp" +#include "SimplnxCore/Filters/QuickSurfaceMeshFilter.hpp" +#include "SimplnxCore/Filters/SurfaceNetsFilter.hpp" +#include "SimplnxCore/SimplnxCore_test_dirs.hpp" + +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" +#include "simplnx/Parameters/MultiArraySelectionParameter.hpp" +#include "simplnx/UnitTest/UnitTestCommon.hpp" + +#include + +using namespace nx::core; +using namespace nx::core::Constants; +using namespace nx::core::UnitTest; + +namespace +{ +const DataPath k_TriangleGeomPath({"TriangleGeom"}); +const DataPath k_TripleLineGeomPath({"Triple Lines"}); +const std::string k_VertexDataName = "Vertex Data"; +const std::string k_FaceDataName = "Face Data"; +const std::string k_NodeTypesName = "NodeTypes"; +const std::string k_FaceLabelsName = "FaceLabels"; +const std::string k_NumFeaturesName = "NumFeatures"; + +const DataPath k_FaceLabelsPath = k_TriangleGeomPath.createChildPath(k_FaceDataName).createChildPath(k_FaceLabelsName); +const DataPath k_NodeTypesPath = k_TriangleGeomPath.createChildPath(k_VertexDataName).createChildPath(k_NodeTypesName); +const DataPath k_OutNumFeaturesPath = k_TripleLineGeomPath.createChildPath(INodeGeometry1D::k_EdgeAttributeMatrixName).createChildPath(k_NumFeaturesName); +const DataPath k_OutNodeTypesPath = k_TripleLineGeomPath.createChildPath(INodeGeometry0D::k_VertexAttributeMatrixName).createChildPath(k_NodeTypesName); + +/** + * @brief Runs QuickSurfaceMesh over the shared 2x2x1 four-grain block, leaving a TriangleGeom at + * k_TriangleGeomPath with its FaceLabels and NodeTypes. The mesher no longer produces triple + * lines itself; extraction is a separate step. + */ +void RunQuickSurfaceMesh(DataStructure& dataStructure) +{ + UnitTest::BuildFourGrainBlock(dataStructure); + + QuickSurfaceMeshFilter filter; + Arguments args; + args.insertOrAssign(QuickSurfaceMeshFilter::k_GridGeometryDataPath_Key, std::make_any(DataPath({"ImageGeom"}))); + args.insertOrAssign(QuickSurfaceMeshFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(DataPath({"ImageGeom", "Cell Data", "FeatureIds"}))); + args.insertOrAssign(QuickSurfaceMeshFilter::k_SelectedDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(QuickSurfaceMeshFilter::k_SelectedFeatureDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(QuickSurfaceMeshFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(k_TriangleGeomPath)); + args.insertOrAssign(QuickSurfaceMeshFilter::k_FixProblemVoxels_Key, std::make_any(false)); + args.insertOrAssign(QuickSurfaceMeshFilter::k_RepairTriangleWinding_Key, std::make_any(false)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); +} + +/** + * @brief Runs ExtractTripleLines over whatever TriangleGeom sits at k_TriangleGeomPath. + */ +IFilter::ExecuteResult RunExtractTripleLines(DataStructure& dataStructure, bool includeExterior = false, const DataPath& triangleGeomPath = k_TriangleGeomPath, + const DataPath& faceLabelsPath = k_FaceLabelsPath, const DataPath& nodeTypesPath = k_NodeTypesPath) +{ + ExtractTripleLinesFilter filter; + Arguments args; + args.insertOrAssign(ExtractTripleLinesFilter::k_TriangleGeometryPath_Key, std::make_any(triangleGeomPath)); + args.insertOrAssign(ExtractTripleLinesFilter::k_FaceLabelsArrayPath_Key, std::make_any(faceLabelsPath)); + args.insertOrAssign(ExtractTripleLinesFilter::k_NodeTypesArrayPath_Key, std::make_any(nodeTypesPath)); + args.insertOrAssign(ExtractTripleLinesFilter::k_IncludeExteriorTripleLines_Key, std::make_any(includeExterior)); + args.insertOrAssign(ExtractTripleLinesFilter::k_CreatedTripleLineGeometryPath_Key, std::make_any(k_TripleLineGeomPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + return filter.execute(dataStructure, args); +} +/** + * @brief Runs SurfaceNets over the same four-grain block, emitting into the same names as + * RunQuickSurfaceMesh so one extraction helper serves both. Smoothing is off so the geometry + * stays predictable. + */ +void RunSurfaceNets(DataStructure& dataStructure) +{ + UnitTest::BuildFourGrainBlock(dataStructure); + + SurfaceNetsFilter filter; + Arguments args; + args.insertOrAssign(SurfaceNetsFilter::k_ApplySmoothing_Key, std::make_any(false)); + args.insertOrAssign(SurfaceNetsFilter::k_RepairTriangleWinding_Key, std::make_any(false)); + args.insertOrAssign(SurfaceNetsFilter::k_MaxDistanceFromVoxelCenter_Key, std::make_any(1.0f)); + args.insertOrAssign(SurfaceNetsFilter::k_RelaxationFactor_Key, std::make_any(0.5f)); + args.insertOrAssign(SurfaceNetsFilter::k_SmoothingIterations_Key, std::make_any(20)); + args.insertOrAssign(SurfaceNetsFilter::k_GridGeometryDataPath_Key, std::make_any(DataPath({"ImageGeom"}))); + args.insertOrAssign(SurfaceNetsFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(DataPath({"ImageGeom", "Cell Data", "FeatureIds"}))); + args.insertOrAssign(SurfaceNetsFilter::k_SelectedDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(SurfaceNetsFilter::k_SelectedFeatureDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(SurfaceNetsFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(k_TriangleGeomPath)); + args.insertOrAssign(SurfaceNetsFilter::k_VertexDataGroupName_Key, std::make_any(k_VertexDataName)); + args.insertOrAssign(SurfaceNetsFilter::k_NodeTypesArrayName_Key, std::make_any(k_NodeTypesName)); + args.insertOrAssign(SurfaceNetsFilter::k_FaceDataGroupName_Key, std::make_any(k_FaceDataName)); + args.insertOrAssign(SurfaceNetsFilter::k_FaceLabelsArrayName_Key, std::make_any(k_FaceLabelsName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); +} + +/** + * @brief Runs M3CSurfaceMeshing over the same four-grain block, emitting into the same names. + */ +void RunM3CSurfaceMeshing(DataStructure& dataStructure) +{ + UnitTest::BuildFourGrainBlock(dataStructure); + + M3CSurfaceMeshingFilter filter; + Arguments args; + args.insertOrAssign(M3CSurfaceMeshingFilter::k_RepairTriangleWinding_Key, std::make_any(true)); + args.insertOrAssign(M3CSurfaceMeshingFilter::k_GridGeometryDataPath_Key, std::make_any(DataPath({"ImageGeom"}))); + args.insertOrAssign(M3CSurfaceMeshingFilter::k_FeatureIdsArrayPath_Key, std::make_any(DataPath({"ImageGeom", "Cell Data", "FeatureIds"}))); + args.insertOrAssign(M3CSurfaceMeshingFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(k_TriangleGeomPath)); + args.insertOrAssign(M3CSurfaceMeshingFilter::k_VertexDataGroupName_Key, std::make_any(k_VertexDataName)); + args.insertOrAssign(M3CSurfaceMeshingFilter::k_NodeTypesArrayName_Key, std::make_any(k_NodeTypesName)); + args.insertOrAssign(M3CSurfaceMeshingFilter::k_FaceDataGroupName_Key, std::make_any(k_FaceDataName)); + args.insertOrAssign(M3CSurfaceMeshingFilter::k_FaceLabelsArrayName_Key, std::make_any(k_FaceLabelsName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); +} +} // namespace + +TEST_CASE("SimplnxCore::ExtractTripleLinesFilter: Quadruple point line from a four-grain block", "[SimplnxCore][ExtractTripleLinesFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + RunQuickSurfaceMesh(dataStructure); + + auto executeResult = RunExtractTripleLines(dataStructure); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_TripleLineGeomPath)); + const auto& tripleLineGeom = dataStructure.getDataRefAs(k_TripleLineGeomPath); + + // Four grains meet along the single interior grid edge, so exactly one segment bordering four + // unique Feature Ids - a quadruple point line. + REQUIRE(tripleLineGeom.getNumberOfEdges() == 1); + REQUIRE(tripleLineGeom.getNumberOfVertices() == 2); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_OutNumFeaturesPath)); + const auto& numFeatures = dataStructure.getDataRefAs(k_OutNumFeaturesPath); + REQUIRE(numFeatures.getNumberOfTuples() == 1); + REQUIRE(numFeatures[0] == 4); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ExtractTripleLinesFilter: Include Exterior Triple Lines", "[SimplnxCore][ExtractTripleLinesFilter]") +{ + UnitTest::LoadPlugins(); + + usize interiorOnlyEdges = 0; + { + DataStructure dataStructure; + RunQuickSurfaceMesh(dataStructure); + auto executeResult = RunExtractTripleLines(dataStructure, false); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_TripleLineGeomPath)); + interiorOnlyEdges = dataStructure.getDataRefAs(k_TripleLineGeomPath).getNumberOfEdges(); + } + + DataStructure dataStructure; + RunQuickSurfaceMesh(dataStructure); + auto executeResult = RunExtractTripleLines(dataStructure, true); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_TripleLineGeomPath)); + const auto& tripleLineGeom = dataStructure.getDataRefAs(k_TripleLineGeomPath); + + // Counting the outside of the volume as a region makes every grain boundary that reaches the + // free surface register as a triple line too, so the count must rise materially. Asserted as a + // strict increase rather than a hardcoded number so the test does not pin an incidental value. + REQUIRE(interiorOnlyEdges == 1); + REQUIRE(tripleLineGeom.getNumberOfEdges() > interiorOnlyEdges); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_OutNumFeaturesPath)); + const auto& numFeatures = dataStructure.getDataRefAs(k_OutNumFeaturesPath); + for(usize i = 0; i < numFeatures.getNumberOfTuples(); i++) + { + INFO("edge " << i << " reported NumFeatures " << static_cast(numFeatures[i])); + REQUIRE((numFeatures[i] == 3 || numFeatures[i] == 4)); + } + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ExtractTripleLinesFilter: NodeTypes are carried onto the created vertices", "[SimplnxCore][ExtractTripleLinesFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + RunQuickSurfaceMesh(dataStructure); + auto executeResult = RunExtractTripleLines(dataStructure); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_TripleLineGeomPath)); + const auto& tripleLineGeom = dataStructure.getDataRefAs(k_TripleLineGeomPath); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_OutNodeTypesPath)); + const auto& outNodeTypes = dataStructure.getDataRefAs(k_OutNodeTypesPath); + + // The array must exist, be sized to the created vertices, and be usable by a filter that + // requires a Node Type array - which is the whole reason it is emitted. + REQUIRE(outNodeTypes.getNumberOfTuples() == tripleLineGeom.getNumberOfVertices()); + + // Every vertex on a triple line is by definition a junction node, so its junction count (the + // NodeTypes value modulo the +10 surface offset) must be at least 3. + for(usize i = 0; i < outNodeTypes.getNumberOfTuples(); i++) + { + const int32 junctionCount = static_cast(outNodeTypes[i] % 10); + INFO("triple line vertex " << i << " has NodeType " << static_cast(outNodeTypes[i])); + REQUIRE(junctionCount >= 3); + } + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ExtractTripleLinesFilter: Agrees with the source mesh NodeTypes", "[SimplnxCore][ExtractTripleLinesFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + RunQuickSurfaceMesh(dataStructure); + auto executeResult = RunExtractTripleLines(dataStructure); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Cross-checks two independently computed quantities: NodeTypes comes from the mesher's own + // junction logic, the triple lines come from FaceLabels. A disagreement points at the NodeTypes + // producer, not at the extraction. + UnitTest::CheckTripleLineNodeTypeAgreement(dataStructure, k_TripleLineGeomPath, k_TriangleGeomPath, k_NodeTypesPath); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ExtractTripleLinesFilter: Preflight rejects mismatched input arrays", "[SimplnxCore][ExtractTripleLinesFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + RunQuickSurfaceMesh(dataStructure); + + ExtractTripleLinesFilter filter; + Arguments args; + args.insertOrAssign(ExtractTripleLinesFilter::k_TriangleGeometryPath_Key, std::make_any(k_TriangleGeomPath)); + args.insertOrAssign(ExtractTripleLinesFilter::k_IncludeExteriorTripleLines_Key, std::make_any(false)); + args.insertOrAssign(ExtractTripleLinesFilter::k_CreatedTripleLineGeometryPath_Key, std::make_any(k_TripleLineGeomPath)); + + SECTION("Face Labels sized to the vertices rather than the faces") + { + // NodeTypes has one tuple per vertex, so handing it in as Face Labels is a realistic + // mis-selection. It must be caught in preflight, not read past the end during execute. + args.insertOrAssign(ExtractTripleLinesFilter::k_FaceLabelsArrayPath_Key, std::make_any(k_FaceLabelsPath)); + args.insertOrAssign(ExtractTripleLinesFilter::k_NodeTypesArrayPath_Key, std::make_any(k_FaceLabelsPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + } +} + +TEST_CASE("SimplnxCore::ExtractTripleLinesFilter: Consistent across surface meshers", "[SimplnxCore][ExtractTripleLinesFilter]") +{ + UnitTest::LoadPlugins(); + + // The same four-grain block through each mesher, then extraction. The assertions are limited to + // properties that are genuinely mesher-independent: segment COUNT is not one of them, because it + // follows node placement and the three meshers place nodes differently (QuickSurfaceMesh on + // voxel corners, SurfaceNets cell-centred, M3C on cube faces). + struct MesherCase + { + const char* Name; + void (*Run)(DataStructure&); + }; + const std::array mesherCases = {MesherCase{"QuickSurfaceMesh", &RunQuickSurfaceMesh}, MesherCase{"SurfaceNets", &RunSurfaceNets}, + MesherCase{"M3CSurfaceMeshing", &RunM3CSurfaceMeshing}}; + + for(const auto& mesherCase : mesherCases) + { + DYNAMIC_SECTION(mesherCase.Name) + { + DataStructure dataStructure; + mesherCase.Run(dataStructure); + + auto executeResult = RunExtractTripleLines(dataStructure); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_TripleLineGeomPath)); + const auto& tripleLineGeom = dataStructure.getDataRefAs(k_TripleLineGeomPath); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_OutNumFeaturesPath)); + const auto& numFeatures = dataStructure.getDataRefAs(k_OutNumFeaturesPath); + + // 1. A junction exists in this input, so every mesher must find at least one segment. + INFO(mesherCase.Name << " produced " << tripleLineGeom.getNumberOfEdges() << " triple line segments"); + REQUIRE(tripleLineGeom.getNumberOfEdges() > 0); + + // 2. Only 3 and 4 are meaningful multiplicities. + bool sawQuadruplePoint = false; + for(usize i = 0; i < numFeatures.getNumberOfTuples(); i++) + { + INFO(mesherCase.Name << " segment " << i << " reported NumFeatures " << static_cast(numFeatures[i])); + REQUIRE((numFeatures[i] == 3 || numFeatures[i] == 4)); + if(numFeatures[i] == 4) + { + sawQuadruplePoint = true; + } + } + + // 3. Four grains meet in this input, so each mesher must find the quadruple point line. + REQUIRE(sawQuadruplePoint); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); + } + } +} + +// M3CSurfaceMeshing emits a triple line vertex at z = -0.5 on this input: a full cell BELOW a +// domain that spans z = 0 to 1, reported as a quadruple point. It also yields 2 segments where +// QuickSurfaceMesh and SurfaceNets both yield 1. +// +// This is a PRE-EXISTING M3C defect, entirely unrelated to triple line extraction, which merely +// made it visible: extraction reads the mesh and FaceLabels that M3C already produced. +// +// The underlying defect is BROADER than what this test pins. M3C's Triangle Geometry itself lies +// partly outside the input volume, at every domain size, not just thin ones. Measured against a +// unit-spacing volume with origin at 0: +// +// domain QuickSurfaceMesh bounds M3CSurfaceMeshing bounds M3C verts outside +// 2x2x1 x[0,2] y[0,2] z[0,1] x[-1,2] y[-1,2] z[-0.5,0.5] 54 of 85 +// 4x4x4 x[0,4] y[0,4] z[0,4] x[-1,4] y[-1,4] z[-0.5,3.5] 142 of 260 +// 8x8x8 x[0,8] y[0,8] z[0,8] x[-1,8] y[-1,8] z[-0.5,7.5] 368 of 744 +// +// Roughly half of every M3C mesh sits outside the volume: x and y extend a full cell to -1, and z +// is offset by half a cell. M3CSurfaceMeshing.cpp's SiteCoords already carries a comment saying it +// subtracts the ghost shell to keep coordinates "aligned with the input volume and the other +// meshers" - so this is a stated intent the code does not achieve, not a deliberate convention. +// +// Note that M3C's exemplar comparison test compares vertex coordinates exactly, so the exemplar +// has these coordinates baked in; fixing M3C means regenerating it. +// +// The [!shouldfail] tag keeps the suite green while pinning the defect, following the precedent in +// ComputeFeatureSizesTest.cpp. When M3C is fixed this test will start passing, at which point +// Catch2 will report it as a failure and the tag must be REMOVED. +// +// CHECK (not REQUIRE) on the segment count so the z-bound assertions below are still evaluated +// rather than the case aborting at the first failure. +TEST_CASE("SimplnxCore::ExtractTripleLinesFilter: M3C triple lines stay within the domain bounds", "[SimplnxCore][ExtractTripleLinesFilter][M3CSurfaceMeshingFilter][!shouldfail]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + RunM3CSurfaceMeshing(dataStructure); + + auto executeResult = RunExtractTripleLines(dataStructure); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_TripleLineGeomPath)); + const auto& tripleLineGeom = dataStructure.getDataRefAs(k_TripleLineGeomPath); + + // The four cells meet along a single interior vertical grid edge. + CHECK(tripleLineGeom.getNumberOfEdges() == 1); + + // No vertex may lie outside the domain, which spans z = 0 to 1. + constexpr float32 k_Epsilon = 0.0001f; + const auto& vertsRef = tripleLineGeom.getVertices()->getDataStoreRef(); + for(usize i = 0; i < tripleLineGeom.getNumberOfVertices(); i++) + { + const float32 z = vertsRef[i * 3 + 2]; + INFO("triple line vertex " << i << " has z = " << z); + CHECK(z >= (0.0f - k_Epsilon)); + CHECK(z <= (1.0f + k_Epsilon)); + } +} diff --git a/src/Plugins/SimplnxCore/test/M3CSurfaceMeshingTest.cpp b/src/Plugins/SimplnxCore/test/M3CSurfaceMeshingTest.cpp index ce61300e3e..3e62f4a92e 100644 --- a/src/Plugins/SimplnxCore/test/M3CSurfaceMeshingTest.cpp +++ b/src/Plugins/SimplnxCore/test/M3CSurfaceMeshingTest.cpp @@ -5,7 +5,9 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" #include "simplnx/DataStructure/Geometry/INodeGeometry0D.hpp" +#include "simplnx/DataStructure/Geometry/INodeGeometry1D.hpp" #include "simplnx/DataStructure/Geometry/INodeGeometry2D.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/Geometry/RectGridGeom.hpp" @@ -926,3 +928,24 @@ TEST_CASE("SimplnxCore::M3CSurfaceMeshingFilter: Bounding Box Skin agrees across UnitTest::CheckArraysInheritTupleDims(serialWindowedResult.Structure); UnitTest::CheckArraysInheritTupleDims(parallelWindowedResult.Structure); } +// Characterization test for an OPEN BUG in M3CSurfaceMeshing, pre-existing and unrelated to triple-line +// generation itself (triple-line extraction only made it visible). +// +// On a 2x2x1 four-grain block (domain spanning z in [0, 1]), M3C emits a spurious extra triple-line +// vertex/edge at z = -0.5 -- one full cell BELOW the domain -- carrying NumFeatures == 4 as if it were a +// genuine quadruple-point junction. The real physical quadruple line is a single vertical segment +// through the shared corner of the four grains (1 edge); M3C instead reports 2 collinear edges split at +// z = 0, with the extra segment/vertex sitting in what can only be a ghost/padding cell layer that M3C +// synthesizes to close the marching-cubes cube on a domain that is only 1 cell thick in Z. +// +// Likely cause: M3C's ghost-shell handling clamps/replicates real feature labels into the padding layer +// on a 1-cell-thick domain instead of using a background label, so the padding cube looks like an +// ordinary interior 4-feature junction to the exterior-triple-line filtering logic. +// +// This is a PRE-EXISTING M3C defect: this branch changed nothing in M3C's meshing (it only appended a +// triple-line-generation call at the end of finalizeMesh), so M3C's mesh and FaceLabels here are +// byte-identical to before this branch. The assertions below encode the CORRECT expected behavior (an +// EdgeGeom confined to the real domain, z in [0, 1]); they currently fail against the actual (buggy) +// output. The [!shouldfail] tag makes Catch2 report that as an expected failure so CI stays green. +// Once M3C's ghost-shell handling is fixed, this test will start passing -- at that point remove the +// [!shouldfail] tag so it becomes a normal regression test. diff --git a/src/Plugins/SimplnxCore/test/QuickSurfaceMeshFilterTest.cpp b/src/Plugins/SimplnxCore/test/QuickSurfaceMeshFilterTest.cpp index 6b0c56bbad..83578ff5e8 100644 --- a/src/Plugins/SimplnxCore/test/QuickSurfaceMeshFilterTest.cpp +++ b/src/Plugins/SimplnxCore/test/QuickSurfaceMeshFilterTest.cpp @@ -1,9 +1,16 @@ #include "SimplnxCore/Filters/QuickSurfaceMeshFilter.hpp" +#include "SimplnxCore/Filters/M3CSurfaceMeshingFilter.hpp" +#include "SimplnxCore/Filters/SurfaceNetsFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataStore.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" #include "simplnx/DataStructure/Geometry/INodeGeometry0D.hpp" +#include "simplnx/DataStructure/Geometry/INodeGeometry1D.hpp" #include "simplnx/DataStructure/Geometry/INodeGeometry2D.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" @@ -66,7 +73,6 @@ TEST_CASE("SimplnxCore::QuickSurfaceMeshFilter", "[SimplnxCore][QuickSurfaceMesh } // Create default Parameters for the filter. - // args.insertOrAssign(QuickSurfaceMeshFilter::k_GenerateTripleLines_Key, std::make_any(false)); args.insertOrAssign(QuickSurfaceMeshFilter::k_FixProblemVoxels_Key, std::make_any(false)); args.insertOrAssign(QuickSurfaceMeshFilter::k_RepairTriangleWinding_Key, std::make_any(false)); @@ -160,7 +166,6 @@ TEST_CASE("SimplnxCore::QuickSurfaceMeshFilter: Winding", "[SimplnxCore][QuickSu } // Create default Parameters for the filter. - // args.insertOrAssign(QuickSurfaceMeshFilter::k_GenerateTripleLines_Key, std::make_any(false)); args.insertOrAssign(QuickSurfaceMeshFilter::k_FixProblemVoxels_Key, std::make_any(false)); args.insertOrAssign(QuickSurfaceMeshFilter::k_RepairTriangleWinding_Key, std::make_any(true)); @@ -254,7 +259,6 @@ TEST_CASE("SimplnxCore::QuickSurfaceMeshFilter: Problem Voxels", "[SimplnxCore][ } // Create default Parameters for the filter. - // args.insertOrAssign(QuickSurfaceMeshFilter::k_GenerateTripleLines_Key, std::make_any(false)); args.insertOrAssign(QuickSurfaceMeshFilter::k_FixProblemVoxels_Key, std::make_any(true)); args.insertOrAssign(QuickSurfaceMeshFilter::k_RepairTriangleWinding_Key, std::make_any(false)); @@ -348,7 +352,6 @@ TEST_CASE("SimplnxCore::QuickSurfaceMeshFilter: Winding and Problem Voxels", "[S } // Create default Parameters for the filter. - // args.insertOrAssign(QuickSurfaceMeshFilter::k_GenerateTripleLines_Key, std::make_any(false)); args.insertOrAssign(QuickSurfaceMeshFilter::k_FixProblemVoxels_Key, std::make_any(true)); args.insertOrAssign(QuickSurfaceMeshFilter::k_RepairTriangleWinding_Key, std::make_any(true)); diff --git a/src/Plugins/SimplnxCore/test/SurfaceNetsTest.cpp b/src/Plugins/SimplnxCore/test/SurfaceNetsTest.cpp index 82be4163cd..e57169a367 100644 --- a/src/Plugins/SimplnxCore/test/SurfaceNetsTest.cpp +++ b/src/Plugins/SimplnxCore/test/SurfaceNetsTest.cpp @@ -2,6 +2,12 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" #include "SurfaceMeshingTestUtils.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataStore.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/DataStructure/Geometry/INodeGeometry0D.hpp" +#include "simplnx/DataStructure/Geometry/INodeGeometry1D.hpp" +#include "simplnx/DataStructure/Geometry/INodeGeometry2D.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" diff --git a/src/simplnx/Utilities/Meshing/TripleLineUtilities.cpp b/src/simplnx/Utilities/Meshing/TripleLineUtilities.cpp new file mode 100644 index 0000000000..af59ad1602 --- /dev/null +++ b/src/simplnx/Utilities/Meshing/TripleLineUtilities.cpp @@ -0,0 +1,182 @@ +#include "TripleLineUtilities.hpp" + +#include + +#include +#include +#include + +using namespace nx::core; + +namespace +{ +// NumFeatures saturates at 4, matching the NodeTypes convention which also caps at 4. +constexpr uint8 k_MaxFeaturesPerEdge = 4; + +// The edge key packs two 32-bit vertex indices into one 64-bit value, so meshes are +// limited to 2^32 vertices. GenerateTripleLines checks this up front. +constexpr uint64 k_MaxVertexCount = 0xFFFFFFFFULL; + +// Recovers the low vertex index when unpacking an edge key. +constexpr uint64 k_VertexIndexMask = 0xFFFFFFFFULL; + +/** + * @brief The unique Feature Ids bordering one mesh edge. A flat array with linear-scan + * insert rather than a std::set: at most 4 entries are ever kept, and a tree node per + * mesh edge would dominate memory on a multi-million triangle mesh. + */ +struct EdgeFeatureSet +{ + std::array Features{}; + uint8 Count = 0; + + void insert(int32 featureId) + { + for(uint8 i = 0; i < Count; i++) + { + if(Features[i] == featureId) + { + return; + } + } + // Saturate rather than grow. An edge bordering more than 4 regions is not + // geometrically meaningful here, and Count stays a faithful "3 or 4". + if(Count < k_MaxFeaturesPerEdge) + { + Features[Count] = featureId; + Count++; + } + } +}; + +/** + * @brief Packs a vertex pair into an order-independent key. + */ +inline uint64 MakeEdgeKey(uint64 v0, uint64 v1) +{ + return (v0 < v1) ? ((v0 << 32) | v1) : ((v1 << 32) | v0); +} +} // namespace + +namespace nx::core::MeshingUtilities +{ +Result<> GenerateTripleLines(const TriangleGeom& triangleGeom, const Int32AbstractDataStore& faceLabelsRef, const Int8AbstractDataStore& sourceNodeTypesRef, EdgeGeom& tripleLineGeom, + Int8AbstractDataStore& numFeaturesRef, Int8AbstractDataStore& tripleLineNodeTypesRef, const TripleLineOptions& options, const std::atomic_bool& shouldCancel, + const IFilter::MessageHandler& messageHandler) +{ + const usize numVertices = triangleGeom.getNumberOfVertices(); + if(numVertices > k_MaxVertexCount) + { + return MakeErrorResult(-57100, fmt::format("Triple line generation supports meshes with at most {} vertices, but '{}' has {}. The edge lookup key packs two 32-bit vertex indices.", + k_MaxVertexCount, triangleGeom.getName(), numVertices)); + } + + const auto& facesRef = triangleGeom.getFaces()->getDataStoreRef(); + const usize numTriangles = triangleGeom.getNumberOfFaces(); + + messageHandler(IFilter::Message::Type::Info, "Triple Lines: Building edge adjacency..."); + + std::unordered_map edgeMap; + edgeMap.reserve(numTriangles * 3 / 2); + + for(usize triIndex = 0; triIndex < numTriangles; triIndex++) + { + if(shouldCancel) + { + return {}; + } + + const int32 labelA = faceLabelsRef[triIndex * 2 + 0]; + const int32 labelB = faceLabelsRef[triIndex * 2 + 1]; + + const uint64 v0 = facesRef[triIndex * 3 + 0]; + const uint64 v1 = facesRef[triIndex * 3 + 1]; + const uint64 v2 = facesRef[triIndex * 3 + 2]; + + const std::array edgeKeys = {MakeEdgeKey(v0, v1), MakeEdgeKey(v1, v2), MakeEdgeKey(v2, v0)}; + for(const uint64 edgeKey : edgeKeys) + { + EdgeFeatureSet& featureSet = edgeMap[edgeKey]; + // When exterior lines are excluded, -1 is simply never counted, so an edge whose + // only third "region" is the outside of the volume falls back to 2 unique ids. + if(options.IncludeExteriorLines || labelA >= 0) + { + featureSet.insert(labelA); + } + if(options.IncludeExteriorLines || labelB >= 0) + { + featureSet.insert(labelB); + } + } + } + + messageHandler(IFilter::Message::Type::Info, "Triple Lines: Selecting edges..."); + + std::vector keptEdges; // pairs of COMPACTED vertex indices + std::vector keptFeatureCounts; + std::unordered_map vertexRemap; // original vertex index -> compact index + std::vector compactToOriginal; + + for(const auto& [edgeKey, featureSet] : edgeMap) + { + if(shouldCancel) + { + return {}; + } + if(featureSet.Count < 3) + { + continue; + } + + const uint64 vA = edgeKey >> 32; + const uint64 vB = edgeKey & k_VertexIndexMask; + + for(const uint64 originalVertex : {vA, vB}) + { + if(vertexRemap.find(originalVertex) == vertexRemap.end()) + { + vertexRemap[originalVertex] = compactToOriginal.size(); + compactToOriginal.push_back(originalVertex); + } + } + + keptEdges.push_back(vertexRemap[vA]); + keptEdges.push_back(vertexRemap[vB]); + keptFeatureCounts.push_back(static_cast(featureSet.Count)); + } + + const usize numTripleLineEdges = keptFeatureCounts.size(); + const usize numTripleLineVertices = compactToOriginal.size(); + + messageHandler(IFilter::Message::Type::Info, fmt::format("Triple Lines: Writing {} edges and {} vertices...", numTripleLineEdges, numTripleLineVertices)); + + tripleLineGeom.resizeVertexList(numTripleLineVertices); + tripleLineGeom.resizeEdgeList(numTripleLineEdges); + tripleLineGeom.getVertexAttributeMatrix()->resizeTuples({numTripleLineVertices}); + tripleLineGeom.getEdgeAttributeMatrix()->resizeTuples({numTripleLineEdges}); + numFeaturesRef.resizeTuples({numTripleLineEdges}); + tripleLineNodeTypesRef.resizeTuples({numTripleLineVertices}); + + const auto& sourceVertsRef = triangleGeom.getVertices()->getDataStoreRef(); + auto& destVertsRef = tripleLineGeom.getVertices()->getDataStoreRef(); + for(usize i = 0; i < numTripleLineVertices; i++) + { + const uint64 originalVertex = compactToOriginal[i]; + destVertsRef[i * 3 + 0] = sourceVertsRef[originalVertex * 3 + 0]; + destVertsRef[i * 3 + 1] = sourceVertsRef[originalVertex * 3 + 1]; + destVertsRef[i * 3 + 2] = sourceVertsRef[originalVertex * 3 + 2]; + // Carried across unchanged. NodeTypes never influences which edges were selected above. + tripleLineNodeTypesRef[i] = sourceNodeTypesRef[originalVertex]; + } + + auto& destEdgesRef = tripleLineGeom.getEdges()->getDataStoreRef(); + for(usize i = 0; i < numTripleLineEdges; i++) + { + destEdgesRef[i * 2 + 0] = keptEdges[i * 2 + 0]; + destEdgesRef[i * 2 + 1] = keptEdges[i * 2 + 1]; + numFeaturesRef[i] = keptFeatureCounts[i]; + } + + return {}; +} +} // namespace nx::core::MeshingUtilities diff --git a/src/simplnx/Utilities/Meshing/TripleLineUtilities.hpp b/src/simplnx/Utilities/Meshing/TripleLineUtilities.hpp new file mode 100644 index 0000000000..976b5d3d6e --- /dev/null +++ b/src/simplnx/Utilities/Meshing/TripleLineUtilities.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "simplnx/Common/Result.hpp" +#include "simplnx/DataStructure/AbstractDataStore.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" +#include "simplnx/Filter/IFilter.hpp" +#include "simplnx/simplnx_export.hpp" + +#include + +namespace nx::core::MeshingUtilities +{ +/** + * @brief Options controlling triple line extraction. + */ +struct SIMPLNX_EXPORT TripleLineOptions +{ + /** + * @brief When true, the "outside the volume" region (Feature Id -1) is counted as a + * distinct region. This makes grain boundaries that reach the free surface of the + * volume register as triple lines. When false (the default) only interior triple + * lines are produced. + */ + bool IncludeExteriorLines = false; +}; + +/** + * @brief Extracts the triple lines of a multi-material triangle mesh into an Edge Geometry. + * + * A mesh edge is a triple line segment if and only if the set of unique Feature Ids across + * the FaceLabels of every triangle sharing that edge has 3 or more members. Feature Id -1 + * ("outside the volume") is counted only when options.IncludeExteriorLines is true. + * + * NOTE: NodeTypes is deliberately NOT consulted as part of the rule, and the number of + * triangles sharing an edge is deliberately not used as the criterion. See + * docs/superpowers/specs/2026-08-19-triple-line-generation-design.md for why. + * + * sourceNodeTypesStore is COPIED THROUGH onto the output vertices and is never read to decide + * whether an edge is a triple line. Do not be tempted to gate the rule on it - that alternative + * was considered and rejected because NodeTypes is produced by three different code paths while + * FaceLabels is a single harmonized convention. + * + * The output Edge Geometry is self-contained: its vertex list is a compacted copy holding + * only the vertices used by triple line edges, so it shares no DataObject with triangleGeom. + * + * NOTE: Edge and vertex ordering follows std::unordered_map iteration order, which is + * deterministic for a given build but is NOT guaranteed stable across standard-library + * implementations. Anyone building an exemplar .dream3d comparison against this Edge + * Geometry should sort the edges/vertices first rather than relying on ordering. + * + * @param triangleGeom The source mesh. Not modified. + * @param faceLabelsRef The per-triangle Feature Id pairs. 2 components, int32. + * @param sourceNodeTypesRef The source mesh's per-vertex NodeTypes. Copied through only. + * @param tripleLineGeom The Edge Geometry to populate. Resized by this function. + * @param numFeaturesRef Per-edge unique Feature Id count. Resized by this function. + * Saturates at 4, matching the NodeTypes convention. + * @param tripleLineNodeTypesRef Per-vertex NodeTypes for the output, copied from + * sourceNodeTypesRef. Resized by this function. Lets the resulting Edge Geometry be + * consumed directly by filters that require a Node Type array, such as Laplacian + * Smoothing. + * @param options See TripleLineOptions. + * @param shouldCancel Checked periodically; on cancel the function returns early. + * @param messageHandler Progress destination. + * @return Result<> Invalid if the mesh has too many vertices for the edge key packing. + */ +SIMPLNX_EXPORT Result<> GenerateTripleLines(const TriangleGeom& triangleGeom, const Int32AbstractDataStore& faceLabelsRef, const Int8AbstractDataStore& sourceNodeTypesRef, EdgeGeom& tripleLineGeom, + Int8AbstractDataStore& numFeaturesRef, Int8AbstractDataStore& tripleLineNodeTypesRef, const TripleLineOptions& options, + const std::atomic_bool& shouldCancel, const IFilter::MessageHandler& messageHandler); +} // namespace nx::core::MeshingUtilities diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d247f189fb..d4fbbd5706 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -47,6 +47,7 @@ add_executable(simplnx_test UuidTest.cpp Vec3Test.cpp StringUtilitiesTest.cpp + TripleLineUtilitiesTest.cpp ScaleBarRendererTest.cpp FilterValidationTest.cpp SimplJsonConversionTest.cpp diff --git a/test/TripleLineUtilitiesTest.cpp b/test/TripleLineUtilitiesTest.cpp new file mode 100644 index 0000000000..016d62f79f --- /dev/null +++ b/test/TripleLineUtilitiesTest.cpp @@ -0,0 +1,389 @@ +#include "simplnx/Utilities/Meshing/TripleLineUtilities.hpp" +#include "simplnx/DataStructure/DataStore.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" + +#include + +#include + +using namespace nx::core; + +namespace +{ +const std::string k_TriangleGeomName = "TriangleGeom"; +const std::string k_FaceLabelsName = "FaceLabels"; +const std::string k_TripleLineGeomName = "TripleLines"; +const std::string k_NumFeaturesName = "NumFeatures"; +const std::string k_NodeTypesName = "NodeTypes"; + +/** + * @brief Builds a TriangleGeom plus its FaceLabels array from explicit vertex, triangle and + * label lists. Returns the geometry; the FaceLabels array is created as its child. + */ +TriangleGeom* CreateTriangleMesh(DataStructure& dataStructure, const std::vector>& vertices, const std::vector>& triangles, + const std::vector>& faceLabels) +{ + REQUIRE(triangles.size() == faceLabels.size()); + + auto* triangleGeom = TriangleGeom::Create(dataStructure, k_TriangleGeomName); + REQUIRE(triangleGeom != nullptr); + + auto vertexStore = std::make_unique>(std::vector{vertices.size()}, std::vector{3}, 0.0f); + auto* vertexArray = IGeometry::SharedVertexList::Create(dataStructure, "SharedVertexList", std::move(vertexStore), triangleGeom->getId()); + REQUIRE(vertexArray != nullptr); + auto& verticesRef = vertexArray->getDataStoreRef(); + for(usize i = 0; i < vertices.size(); i++) + { + verticesRef[i * 3 + 0] = vertices[i][0]; + verticesRef[i * 3 + 1] = vertices[i][1]; + verticesRef[i * 3 + 2] = vertices[i][2]; + } + triangleGeom->setVertices(*vertexArray); + + auto faceStore = std::make_unique>(std::vector{triangles.size()}, std::vector{3}, 0); + auto* faceArray = IGeometry::SharedFaceList::Create(dataStructure, "SharedTriList", std::move(faceStore), triangleGeom->getId()); + REQUIRE(faceArray != nullptr); + auto& facesRef = faceArray->getDataStoreRef(); + for(usize i = 0; i < triangles.size(); i++) + { + facesRef[i * 3 + 0] = triangles[i][0]; + facesRef[i * 3 + 1] = triangles[i][1]; + facesRef[i * 3 + 2] = triangles[i][2]; + } + triangleGeom->setFaceList(*faceArray); + + // Every source vertex gets a NodeTypes value. GenerateTripleLines copies these through to the + // output vertices; it never reads them to decide which edges are triple lines. + auto nodeTypeStore = std::make_unique>(std::vector{vertices.size()}, std::vector{1}, 0); + auto* nodeTypeArray = Int8Array::Create(dataStructure, k_NodeTypesName, std::move(nodeTypeStore), triangleGeom->getId()); + REQUIRE(nodeTypeArray != nullptr); + auto& nodeTypesRef = nodeTypeArray->getDataStoreRef(); + for(usize i = 0; i < vertices.size(); i++) + { + // Distinct, recognisable values so a copy-through bug is visible rather than masked by zeros. + nodeTypesRef[i] = static_cast(2 + (i % 3)); + } + + auto labelStore = std::make_unique>(std::vector{faceLabels.size()}, std::vector{2}, 0); + auto* labelArray = Int32Array::Create(dataStructure, k_FaceLabelsName, std::move(labelStore), triangleGeom->getId()); + REQUIRE(labelArray != nullptr); + auto& labelsRef = labelArray->getDataStoreRef(); + for(usize i = 0; i < faceLabels.size(); i++) + { + labelsRef[i * 2 + 0] = faceLabels[i][0]; + labelsRef[i * 2 + 1] = faceLabels[i][1]; + } + + return triangleGeom; +} + +/** + * @brief Creates an empty EdgeGeom with its attribute matrices, plus a NumFeatures array. + * GenerateTripleLines resizes all of them. + */ +std::tuple CreateEmptyTripleLineGeom(DataStructure& dataStructure) +{ + auto* edgeGeom = EdgeGeom::Create(dataStructure, k_TripleLineGeomName); + REQUIRE(edgeGeom != nullptr); + + auto vertexStore = std::make_unique>(std::vector{0}, std::vector{3}, 0.0f); + auto* vertexArray = IGeometry::SharedVertexList::Create(dataStructure, "SharedVertexList", std::move(vertexStore), edgeGeom->getId()); + REQUIRE(vertexArray != nullptr); + edgeGeom->setVertices(*vertexArray); + + auto edgeStore = std::make_unique>(std::vector{0}, std::vector{2}, 0); + auto* edgeArray = IGeometry::SharedEdgeList::Create(dataStructure, "SharedEdgeList", std::move(edgeStore), edgeGeom->getId()); + REQUIRE(edgeArray != nullptr); + edgeGeom->setEdgeList(*edgeArray); + + auto* vertexAM = AttributeMatrix::Create(dataStructure, "Vertex Data", ShapeType{0}, edgeGeom->getId()); + REQUIRE(vertexAM != nullptr); + edgeGeom->setVertexAttributeMatrix(*vertexAM); + + auto* edgeAM = AttributeMatrix::Create(dataStructure, "Edge Data", ShapeType{0}, edgeGeom->getId()); + REQUIRE(edgeAM != nullptr); + edgeGeom->setEdgeAttributeMatrix(*edgeAM); + + auto numFeaturesStore = std::make_unique>(std::vector{0}, std::vector{1}, 0); + auto* numFeaturesArray = Int8Array::Create(dataStructure, k_NumFeaturesName, std::move(numFeaturesStore), edgeAM->getId()); + REQUIRE(numFeaturesArray != nullptr); + + auto outNodeTypeStore = std::make_unique>(std::vector{0}, std::vector{1}, 0); + auto* outNodeTypeArray = Int8Array::Create(dataStructure, k_NodeTypesName, std::move(outNodeTypeStore), vertexAM->getId()); + REQUIRE(outNodeTypeArray != nullptr); + + return {edgeGeom, numFeaturesArray, outNodeTypeArray}; +} +} // namespace + +TEST_CASE("MeshingUtilities::GenerateTripleLines: Flat boundary produces no triple lines", "[Core][MeshingUtilities]") +{ + DataStructure dataStructure; + + // A single quad between grains 1 and 2, split into two triangles. Every edge is + // shared by at most two triangles, and both carry the same labels, so no edge + // borders 3 or more unique Feature Ids. + const std::vector> vertices = {{0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}}; + const std::vector> triangles = {{0, 1, 2}, {0, 2, 3}}; + const std::vector> faceLabels = {{1, 2}, {1, 2}}; + + auto* triangleGeom = CreateTriangleMesh(dataStructure, vertices, triangles, faceLabels); + auto [edgeGeom, numFeaturesArray, tripleLineNodeTypes] = CreateEmptyTripleLineGeom(dataStructure); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName}))); + const auto& faceLabelsRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName})).getDataStoreRef(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName}))); + const auto& sourceNodeTypesRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName})).getDataStoreRef(); + + const std::atomic_bool shouldCancel{false}; + MeshingUtilities::TripleLineOptions options; + + Result<> result = MeshingUtilities::GenerateTripleLines(*triangleGeom, faceLabelsRef, sourceNodeTypesRef, *edgeGeom, numFeaturesArray->getDataStoreRef(), tripleLineNodeTypes->getDataStoreRef(), + options, shouldCancel, {}); + + REQUIRE(result.valid()); + REQUIRE(edgeGeom->getNumberOfEdges() == 0); + REQUIRE(edgeGeom->getNumberOfVertices() == 0); + REQUIRE(numFeaturesArray->getNumberOfTuples() == 0); +} + +TEST_CASE("MeshingUtilities::GenerateTripleLines: Interior triple junction", "[Core][MeshingUtilities]") +{ + DataStructure dataStructure; + + // Three quad sheets meeting along the shared edge v0->v1. Each sheet is split so that + // exactly one of its two triangles contains both v0 and v1, so the shared edge is + // touched by exactly 3 triangles carrying labels {1,2}, {2,3} and {1,3} => 3 unique. + const std::vector> vertices = { + {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, // v0, v1 : the shared edge + {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, // v2, v3 : sheet A rim + {-0.5f, 0.87f, 0.0f}, {-0.5f, 0.87f, 1.0f}, // v4, v5 : sheet B rim + {-0.5f, -0.87f, 0.0f}, {-0.5f, -0.87f, 1.0f}, // v6, v7 : sheet C rim + }; + const std::vector> triangles = { + {0, 1, 3}, {0, 3, 2}, // sheet A + {0, 1, 5}, {0, 5, 4}, // sheet B + {0, 1, 7}, {0, 7, 6}, // sheet C + }; + const std::vector> faceLabels = { + {1, 2}, {1, 2}, // sheet A + {2, 3}, {2, 3}, // sheet B + {1, 3}, {1, 3}, // sheet C + }; + + auto* triangleGeom = CreateTriangleMesh(dataStructure, vertices, triangles, faceLabels); + auto [edgeGeom, numFeaturesArray, tripleLineNodeTypes] = CreateEmptyTripleLineGeom(dataStructure); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName}))); + const auto& faceLabelsRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName})).getDataStoreRef(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName}))); + const auto& sourceNodeTypesRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName})).getDataStoreRef(); + + const std::atomic_bool shouldCancel{false}; + MeshingUtilities::TripleLineOptions options; + + Result<> result = MeshingUtilities::GenerateTripleLines(*triangleGeom, faceLabelsRef, sourceNodeTypesRef, *edgeGeom, numFeaturesArray->getDataStoreRef(), tripleLineNodeTypes->getDataStoreRef(), + options, shouldCancel, {}); + + REQUIRE(result.valid()); + REQUIRE(edgeGeom->getNumberOfEdges() == 1); + REQUIRE(edgeGeom->getNumberOfVertices() == 2); + REQUIRE(numFeaturesArray->getNumberOfTuples() == 1); + REQUIRE((*numFeaturesArray)[0] == 3); + + // The one emitted edge must join the two ends of the shared edge, which sit at + // z = 0 and z = 1 with x = y = 0. Vertex order within the edge is unspecified. + const auto& edgesRef = edgeGeom->getEdges()->getDataStoreRef(); + const auto& vertsRef = edgeGeom->getVertices()->getDataStoreRef(); + std::set zCoords; + for(usize i = 0; i < 2; i++) + { + const usize vertIndex = edgesRef[i]; + REQUIRE(vertsRef[vertIndex * 3 + 0] == Approx(0.0f)); + REQUIRE(vertsRef[vertIndex * 3 + 1] == Approx(0.0f)); + zCoords.insert(vertsRef[vertIndex * 3 + 2]); + } + REQUIRE(zCoords == std::set{0.0f, 1.0f}); +} + +TEST_CASE("MeshingUtilities::GenerateTripleLines: Quadruple point line", "[Core][MeshingUtilities]") +{ + DataStructure dataStructure; + + // Four sheets around the shared edge, labels {1,2}, {2,3}, {3,4} and {1,4} => 4 unique. + const std::vector> vertices = { + {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, // v0, v1 : the shared edge + {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, // sheet A rim + {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 1.0f}, // sheet B rim + {-1.0f, 0.0f, 0.0f}, {-1.0f, 0.0f, 1.0f}, // sheet C rim + {0.0f, -1.0f, 0.0f}, {0.0f, -1.0f, 1.0f}, // sheet D rim + }; + const std::vector> triangles = { + {0, 1, 3}, {0, 3, 2}, {0, 1, 5}, {0, 5, 4}, {0, 1, 7}, {0, 7, 6}, {0, 1, 9}, {0, 9, 8}, + }; + const std::vector> faceLabels = { + {1, 2}, {1, 2}, {2, 3}, {2, 3}, {3, 4}, {3, 4}, {1, 4}, {1, 4}, + }; + + auto* triangleGeom = CreateTriangleMesh(dataStructure, vertices, triangles, faceLabels); + auto [edgeGeom, numFeaturesArray, tripleLineNodeTypes] = CreateEmptyTripleLineGeom(dataStructure); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName}))); + const auto& faceLabelsRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName})).getDataStoreRef(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName}))); + const auto& sourceNodeTypesRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName})).getDataStoreRef(); + + const std::atomic_bool shouldCancel{false}; + MeshingUtilities::TripleLineOptions options; + + Result<> result = MeshingUtilities::GenerateTripleLines(*triangleGeom, faceLabelsRef, sourceNodeTypesRef, *edgeGeom, numFeaturesArray->getDataStoreRef(), tripleLineNodeTypes->getDataStoreRef(), + options, shouldCancel, {}); + + REQUIRE(result.valid()); + REQUIRE(edgeGeom->getNumberOfEdges() == 1); + REQUIRE(numFeaturesArray->getNumberOfTuples() == 1); + REQUIRE((*numFeaturesArray)[0] == 4); +} + +TEST_CASE("MeshingUtilities::GenerateTripleLines: Vertex list is compacted", "[Core][MeshingUtilities]") +{ + DataStructure dataStructure; + + // Same triple junction as Task 2, which has 8 source vertices but only 2 lie on a + // triple line. The output must carry exactly those 2, and every edge index must be + // in range - i.e. the remap really happened rather than passing indices through. + const std::vector> vertices = { + {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, {-0.5f, 0.87f, 0.0f}, {-0.5f, 0.87f, 1.0f}, {-0.5f, -0.87f, 0.0f}, {-0.5f, -0.87f, 1.0f}, + }; + const std::vector> triangles = {{0, 1, 3}, {0, 3, 2}, {0, 1, 5}, {0, 5, 4}, {0, 1, 7}, {0, 7, 6}}; + const std::vector> faceLabels = {{1, 2}, {1, 2}, {2, 3}, {2, 3}, {1, 3}, {1, 3}}; + + auto* triangleGeom = CreateTriangleMesh(dataStructure, vertices, triangles, faceLabels); + auto [edgeGeom, numFeaturesArray, tripleLineNodeTypes] = CreateEmptyTripleLineGeom(dataStructure); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName}))); + const auto& faceLabelsRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName})).getDataStoreRef(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName}))); + const auto& sourceNodeTypesRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName})).getDataStoreRef(); + + const std::atomic_bool shouldCancel{false}; + MeshingUtilities::TripleLineOptions options; + + Result<> result = MeshingUtilities::GenerateTripleLines(*triangleGeom, faceLabelsRef, sourceNodeTypesRef, *edgeGeom, numFeaturesArray->getDataStoreRef(), tripleLineNodeTypes->getDataStoreRef(), + options, shouldCancel, {}); + + REQUIRE(result.valid()); + REQUIRE(triangleGeom->getNumberOfVertices() == 8); + REQUIRE(edgeGeom->getNumberOfVertices() == 2); + + const auto& edgesRef = edgeGeom->getEdges()->getDataStoreRef(); + for(usize i = 0; i < edgeGeom->getNumberOfEdges() * 2; i++) + { + REQUIRE(edgesRef[i] < edgeGeom->getNumberOfVertices()); + } +} + +TEST_CASE("MeshingUtilities::GenerateTripleLines: IncludeExteriorLines toggles surface lines", "[Core][MeshingUtilities]") +{ + // A grain boundary between grains 1 and 2 reaching the free surface of the volume. + // Three sheets meet along the shared edge: the interior 1|2 boundary, and the two + // exposed outer faces of grains 1 and 2, which carry the -1 "outside" label. + const std::vector> vertices = { + {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, {-0.5f, 0.87f, 0.0f}, {-0.5f, 0.87f, 1.0f}, {-0.5f, -0.87f, 0.0f}, {-0.5f, -0.87f, 1.0f}, + }; + const std::vector> triangles = {{0, 1, 3}, {0, 3, 2}, {0, 1, 5}, {0, 5, 4}, {0, 1, 7}, {0, 7, 6}}; + const std::vector> faceLabels = {{1, 2}, {1, 2}, {-1, 2}, {-1, 2}, {-1, 1}, {-1, 1}}; + + SECTION("Interior only (the default) rejects it") + { + DataStructure dataStructure; + auto* triangleGeom = CreateTriangleMesh(dataStructure, vertices, triangles, faceLabels); + auto [edgeGeom, numFeaturesArray, tripleLineNodeTypes] = CreateEmptyTripleLineGeom(dataStructure); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName}))); + const auto& faceLabelsRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName})).getDataStoreRef(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName}))); + const auto& sourceNodeTypesRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName})).getDataStoreRef(); + + const std::atomic_bool shouldCancel{false}; + MeshingUtilities::TripleLineOptions options; + options.IncludeExteriorLines = false; + + Result<> result = MeshingUtilities::GenerateTripleLines(*triangleGeom, faceLabelsRef, sourceNodeTypesRef, *edgeGeom, numFeaturesArray->getDataStoreRef(), tripleLineNodeTypes->getDataStoreRef(), + options, shouldCancel, {}); + + // Discounting -1, the shared edge borders only grains 1 and 2. + REQUIRE(result.valid()); + REQUIRE(edgeGeom->getNumberOfEdges() == 0); + } + + SECTION("IncludeExteriorLines accepts it") + { + DataStructure dataStructure; + auto* triangleGeom = CreateTriangleMesh(dataStructure, vertices, triangles, faceLabels); + auto [edgeGeom, numFeaturesArray, tripleLineNodeTypes] = CreateEmptyTripleLineGeom(dataStructure); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName}))); + const auto& faceLabelsRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName})).getDataStoreRef(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName}))); + const auto& sourceNodeTypesRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName})).getDataStoreRef(); + + const std::atomic_bool shouldCancel{false}; + MeshingUtilities::TripleLineOptions options; + options.IncludeExteriorLines = true; + + Result<> result = MeshingUtilities::GenerateTripleLines(*triangleGeom, faceLabelsRef, sourceNodeTypesRef, *edgeGeom, numFeaturesArray->getDataStoreRef(), tripleLineNodeTypes->getDataStoreRef(), + options, shouldCancel, {}); + + // Counting -1 as a region, the shared edge borders {1, 2, -1} => 3 unique. + REQUIRE(result.valid()); + REQUIRE(edgeGeom->getNumberOfEdges() == 1); + REQUIRE((*numFeaturesArray)[0] == 3); + } +} + +TEST_CASE("MeshingUtilities::GenerateTripleLines: NodeTypes are copied through to the output vertices", "[Core][MeshingUtilities]") +{ + DataStructure dataStructure; + + // The Task 2 triple junction: 8 source vertices, of which only v0 and v1 lie on the triple line. + const std::vector> vertices = { + {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 1.0f}, {-0.5f, 0.87f, 0.0f}, {-0.5f, 0.87f, 1.0f}, {-0.5f, -0.87f, 0.0f}, {-0.5f, -0.87f, 1.0f}, + }; + const std::vector> triangles = {{0, 1, 3}, {0, 3, 2}, {0, 1, 5}, {0, 5, 4}, {0, 1, 7}, {0, 7, 6}}; + const std::vector> faceLabels = {{1, 2}, {1, 2}, {2, 3}, {2, 3}, {1, 3}, {1, 3}}; + + auto* triangleGeom = CreateTriangleMesh(dataStructure, vertices, triangles, faceLabels); + auto [edgeGeom, numFeaturesArray, tripleLineNodeTypes] = CreateEmptyTripleLineGeom(dataStructure); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName}))); + const auto& faceLabelsRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_FaceLabelsName})).getDataStoreRef(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName}))); + const auto& sourceNodeTypesRef = dataStructure.getDataRefAs(DataPath({k_TriangleGeomName, k_NodeTypesName})).getDataStoreRef(); + + const std::atomic_bool shouldCancel{false}; + MeshingUtilities::TripleLineOptions options; + + Result<> result = MeshingUtilities::GenerateTripleLines(*triangleGeom, faceLabelsRef, sourceNodeTypesRef, *edgeGeom, numFeaturesArray->getDataStoreRef(), tripleLineNodeTypes->getDataStoreRef(), + options, shouldCancel, {}); + + REQUIRE(result.valid()); + REQUIRE(edgeGeom->getNumberOfVertices() == 2); + REQUIRE(tripleLineNodeTypes->getNumberOfTuples() == 2); + + // Each output vertex must carry the NodeTypes value of the SOURCE vertex it was copied from. + // The fixture assigns nodeTypes[i] = 2 + (i % 3), and only source vertices 0 and 1 survive + // compaction, so the two output values must be exactly {2, 3} — matched by coordinate, since + // compaction does not preserve index order. + const auto& vertsRef = edgeGeom->getVertices()->getDataStoreRef(); + const auto& outNodeTypesRef = tripleLineNodeTypes->getDataStoreRef(); + for(usize i = 0; i < 2; i++) + { + const float32 z = vertsRef[i * 3 + 2]; + const usize originalIndex = (z == Approx(0.0f)) ? 0 : 1; + INFO("output vertex " << i << " at z=" << z << " maps to source vertex " << originalIndex); + REQUIRE(outNodeTypesRef[i] == sourceNodeTypesRef[originalIndex]); + } +} diff --git a/test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp b/test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp index 7a73d41a84..d1a109c4d1 100644 --- a/test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp +++ b/test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp @@ -3,6 +3,7 @@ #include "simplnx/Common/Result.hpp" #include "simplnx/Common/StringLiteral.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataGroup.hpp" #include "simplnx/DataStructure/DataObject.hpp" #include "simplnx/DataStructure/DataStore.hpp" @@ -1694,6 +1695,79 @@ inline void CheckArraysInheritTupleDims(const DataStructure& dataStructure, cons } } +/** + * @brief Asserts that every vertex on a triple line also reports a junction count of 3 or + * more in NodeTypes. The two quantities are computed by completely independent paths - + * NodeTypes from the mesher's own junction logic, triple lines from FaceLabels - so + * agreement is a real signal. NodeTypes adds 10 for surface nodes, hence the % 10. + * + * A failure here indicates the NodeTypes producer is wrong, not GenerateTripleLines. + * + * Cost: O(triple-line vertices x mesh vertices) with exact coordinate matching, so this + * helper is intended for small fixtures only - not for full datasets. + */ +inline void CheckTripleLineNodeTypeAgreement(const DataStructure& dataStructure, const DataPath& tripleLineGeomPath, const DataPath& triangleGeomPath, const DataPath& nodeTypesPath) +{ + REQUIRE_NOTHROW(dataStructure.getDataRefAs(tripleLineGeomPath)); + const auto& tripleLineGeom = dataStructure.getDataRefAs(tripleLineGeomPath); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(triangleGeomPath)); + const auto& triangleGeom = dataStructure.getDataRefAs(triangleGeomPath); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(nodeTypesPath)); + const auto& nodeTypes = dataStructure.getDataRefAs(nodeTypesPath); + + const auto& tripleLineVertsRef = tripleLineGeom.getVertices()->getDataStoreRef(); + const auto& meshVertsRef = triangleGeom.getVertices()->getDataStoreRef(); + + // Triple line vertices are a compacted copy, so match them back by coordinate. + for(usize i = 0; i < tripleLineGeom.getNumberOfVertices(); i++) + { + bool foundMatch = false; + for(usize j = 0; j < triangleGeom.getNumberOfVertices(); j++) + { + if(tripleLineVertsRef[i * 3 + 0] == meshVertsRef[j * 3 + 0] && tripleLineVertsRef[i * 3 + 1] == meshVertsRef[j * 3 + 1] && tripleLineVertsRef[i * 3 + 2] == meshVertsRef[j * 3 + 2]) + { + const int8 junctionCount = static_cast(nodeTypes[j] % 10); + INFO("Triple line vertex " << i << " maps to mesh vertex " << j << " with NodeType " << static_cast(nodeTypes[j])); + REQUIRE(junctionCount >= 3); + foundMatch = true; + break; + } + } + INFO("Triple line vertex " << i << " has no matching mesh vertex"); + REQUIRE(foundMatch); + } +} + +/** + * @brief Builds a 2x2x1 four-grain Image Geometry: every one of the four cells is its own + * Feature, so the four cells meet along one interior grid edge with 4 unique Feature Ids. + * Used by the triple-line tests across QuickSurfaceMesh, SurfaceNets, and M3CSurfaceMeshing. + * + * @param dataStructure The DataStructure to populate. + * @param imageGeomName Name given to the created ImageGeom. + * @param cellDataName Name given to the created Cell Data AttributeMatrix. + * @param featureIdsName Name given to the created FeatureIds Int32Array. + */ +inline void BuildFourGrainBlock(DataStructure& dataStructure, const std::string& imageGeomName = "ImageGeom", const std::string& cellDataName = "Cell Data", + const std::string& featureIdsName = "FeatureIds") +{ + auto* imageGeom = ImageGeom::Create(dataStructure, imageGeomName); + imageGeom->setDimensions({2, 2, 1}); + imageGeom->setSpacing({1.0f, 1.0f, 1.0f}); + imageGeom->setOrigin({0.0f, 0.0f, 0.0f}); + + auto* cellAM = AttributeMatrix::Create(dataStructure, cellDataName, ShapeType{1, 2, 2}, imageGeom->getId()); + imageGeom->setCellData(*cellAM); + + auto featureIdsStore = std::make_unique>(std::vector{1, 2, 2}, std::vector{1}, 0); + auto* featureIds = Int32Array::Create(dataStructure, featureIdsName, std::move(featureIdsStore), cellAM->getId()); + auto& featureIdsRef = featureIds->getDataStoreRef(); + featureIdsRef[0] = 1; + featureIdsRef[1] = 2; + featureIdsRef[2] = 3; + featureIdsRef[3] = 4; +} + namespace Cropping { inline std::string BoolToString(bool v)