From 1e7759ae728789cfb6d77c5c0e995381832b203f Mon Sep 17 00:00:00 2001 From: Nathan Young Date: Tue, 9 Jan 2024 13:40:40 -0500 Subject: [PATCH] ENH: Write STL File - Option to write out the entire triangle geometry in a single file. (#809) This removes the need for the FaceLabels input array. Signed-off-by: Michael Jackson Co-authored-by: Michael Jackson --- .../Filters/Algorithms/WriteStlFile.cpp | 461 ++++++++++++------ .../Filters/Algorithms/WriteStlFile.hpp | 12 +- .../Filters/WriteStlFileFilter.cpp | 43 +- .../Filters/WriteStlFileFilter.hpp | 4 +- src/Plugins/SimplnxCore/test/CMakeLists.txt | 1 + .../SimplnxCore/test/WriteStlFileTest.cpp | 48 +- 6 files changed, 407 insertions(+), 162 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.cpp index f98550fc7a..5aa23517d3 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.cpp @@ -1,207 +1,392 @@ #include "WriteStlFile.hpp" +// NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast, cppcoreguidelines-pro-bounds-pointer-arithmetic) + #include "simplnx/Common/AtomicFile.hpp" #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" +#include "simplnx/Utilities/Math/MatrixMath.hpp" #include "simplnx/Utilities/StringUtilities.hpp" -using namespace nx::core; +#include -// ----------------------------------------------------------------------------- -WriteStlFile::WriteStlFile(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, WriteStlFileInputValues* inputValues) -: m_DataStructure(dataStructure) -, m_InputValues(inputValues) -, m_ShouldCancel(shouldCancel) -, m_MessageHandler(mesgHandler) -{ -} +namespace fs = std::filesystem; -// ----------------------------------------------------------------------------- -WriteStlFile::~WriteStlFile() noexcept = default; +using namespace nx::core; -// ----------------------------------------------------------------------------- -const std::atomic_bool& WriteStlFile::getCancel() +namespace { - return m_ShouldCancel; -} - -// ----------------------------------------------------------------------------- -Result<> WriteStlFile::operator()() +Result<> SingleWriteOutStl(const fs::path& path, const IGeometry::MeshIndexType numTriangles, const std::string&& header, const IGeometry::MeshIndexArrayType& triangles, const Float32Array& vertices) { - const auto& triangleGeom = m_DataStructure.getDataRefAs(m_InputValues->TriangleGeomPath); - const Float32Array& vertices = triangleGeom.getVerticesRef(); - const IGeometry::MeshIndexArrayType& triangles = triangleGeom.getFacesRef(); - const IGeometry::MeshIndexType nTriangles = triangleGeom.getNumberOfFaces(); - const auto& featureIds = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsPath); + Result<> result; - const std::filesystem::path outputPath = m_InputValues->OutputStlDirectory; - // Make sure any directory path is also available as the user may have just typed - // in a path without actually creating the full path - Result<> createDirectoriesResult = nx::core::CreateOutputDirectories(outputPath); - if(createDirectoriesResult.invalid()) + // Create output file writer in binary write out mode to ensure cross-compatibility + FILE* filePtr = fopen(path.string().c_str(), "wb"); + + if(filePtr == nullptr) { - return createDirectoriesResult; + fclose(filePtr); + return {MakeWarningVoidResult(-27886, fmt::format("Error Opening STL File. Unable to create temp file at path '{}' for original file '{}'", path.string(), path.filename().string()))}; } - // Store all the unique Spins - std::map uniqueGrainIdToPhase; - if(m_InputValues->GroupByFeature) - { - const auto& featurePhases = m_DataStructure.getDataRefAs(m_InputValues->FeaturePhasesPath); - for(IGeometry::MeshIndexType i = 0; i < nTriangles; i++) + int32 triCount = 0; + + { // Scope header output processing to keep overhead low and increase readability + if(header.size() >= 80) { - uniqueGrainIdToPhase.emplace(featureIds[i * 2], featurePhases[i * 2]); - uniqueGrainIdToPhase.emplace(featureIds[i * 2 + 1], featurePhases[i * 2 + 1]); + result = MakeWarningVoidResult(-27884, + fmt::format("Warning: Writing STL File '{}'. Header was over the 80 characters supported by STL. Length of header: {}. Only the first 80 bytes will be written.", + path.filename().string(), header.length())); } - } - else - { - for(IGeometry::MeshIndexType i = 0; i < nTriangles; i++) + + std::array stlFileHeader = {}; + stlFileHeader.fill(0); + size_t headLength = 80; + if(header.length() < 80) { - uniqueGrainIdToPhase.emplace(featureIds[i * 2], 0); - uniqueGrainIdToPhase.emplace(featureIds[i * 2 + 1], 0); + headLength = static_cast(header.length()); } + + // std::string c_str = header; + memcpy(stlFileHeader.data(), header.data(), headLength); + // Return the number of bytes written - which should be 80 + fwrite(stlFileHeader.data(), 1, 80, filePtr); } - unsigned char data[50]; - auto* normal = reinterpret_cast(data); - auto* vert1 = reinterpret_cast(data + 12); - auto* vert2 = reinterpret_cast(data + 24); - auto* vert3 = reinterpret_cast(data + 36); - auto* attrByteCount = reinterpret_cast(data + 48); - *attrByteCount = 0; + fwrite(&triCount, 1, 4, filePtr); + triCount = 0; // Reset this to Zero. Increment for every triangle written size_t totalWritten = 0; - float u[3] = {0.0f, 0.0f, 0.0f}, w[3] = {0.0f, 0.0f, 0.0f}; - float length = 0.0f; + std::array vecA = {0.0f, 0.0f, 0.0f}; + std::array vecB = {0.0f, 0.0f, 0.0f}; + + std::array data = {}; + nonstd::span normalPtr(reinterpret_cast(data.data()), 3); + nonstd::span vert1Ptr(reinterpret_cast(data.data() + 12), 3); + nonstd::span vert2Ptr(reinterpret_cast(data.data() + 24), 3); + nonstd::span vert3Ptr(reinterpret_cast(data.data() + 36), 3); + nonstd::span attrByteCountPtr(reinterpret_cast(data.data() + 48), 2); + attrByteCountPtr[0] = 0; + + // Loop over all the triangles for this spin + for(IGeometry::MeshIndexType triangle = 0; triangle < numTriangles; ++triangle) + { + // Get the true indices of the 3 nodes + IGeometry::MeshIndexType nId0 = triangles[triangle * 3]; + IGeometry::MeshIndexType nId1 = triangles[triangle * 3 + 1]; + IGeometry::MeshIndexType nId2 = triangles[triangle * 3 + 2]; - int32_t triCount = 0; + vert1Ptr[0] = static_cast(vertices[nId0 * 3]); + vert1Ptr[1] = static_cast(vertices[nId0 * 3 + 1]); + vert1Ptr[2] = static_cast(vertices[nId0 * 3 + 2]); - // Loop over the unique feature Ids - for(const auto& [featureId, value] : uniqueGrainIdToPhase) - { - // Generate the output file name - std::string filename = m_InputValues->OutputStlDirectory.string() + "/" + m_InputValues->OutputStlPrefix; - if(m_InputValues->GroupByFeature) - { - filename += "Ensemble_" + StringUtilities::number(value) + "_"; - } + vert2Ptr[0] = static_cast(vertices[nId1 * 3]); + vert2Ptr[1] = static_cast(vertices[nId1 * 3 + 1]); + vert2Ptr[2] = static_cast(vertices[nId1 * 3 + 2]); + + vert3Ptr[0] = static_cast(vertices[nId2 * 3]); + vert3Ptr[1] = static_cast(vertices[nId2 * 3 + 1]); + vert3Ptr[2] = static_cast(vertices[nId2 * 3 + 2]); - filename += "Feature_" + StringUtilities::number(featureId) + ".stl"; + // Compute the normal + vecA[0] = vert2Ptr[0] - vert1Ptr[0]; + vecA[1] = vert2Ptr[1] - vert1Ptr[1]; + vecA[2] = vert2Ptr[2] - vert1Ptr[2]; - AtomicFile atomicFile(filename, true); + vecB[0] = vert3Ptr[0] - vert1Ptr[0]; + vecB[1] = vert3Ptr[1] - vert1Ptr[1]; + vecB[2] = vert3Ptr[2] - vert1Ptr[2]; - FILE* f = fopen(atomicFile.tempFilePath().string().c_str(), "wb"); + MatrixMath::CrossProduct(vecA.data(), vecB.data(), normalPtr.data()); + MatrixMath::Normalize3x1(normalPtr.data()); - if(f == nullptr) + totalWritten = fwrite(data.data(), 1, 50, filePtr); + if(totalWritten != 50) { - fclose(f); - atomicFile.setAutoCommit(false); // Set this to false otherwise - return {MakeWarningVoidResult(-27875, fmt::format("Error Opening STL File. Unable to create temp file at path '{}' for original file '{}'", atomicFile.tempFilePath().string(), filename))}; + fclose(filePtr); + return {MakeWarningVoidResult( + -27883, fmt::format("Error Writing STL File '{}'. Not enough elements written for Triangle {}. Wrote {} of 50. No file written.", path.filename().string(), triangle, totalWritten))}; } + triCount++; + } - m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing STL for Feature Id {}", featureId)); + fseek(filePtr, 80L, SEEK_SET); + fwrite(reinterpret_cast(&triCount), 1, 4, filePtr); + fclose(filePtr); + return result; +} - std::string header = "DREAM3D Generated For Feature ID " + StringUtilities::number(featureId); - if(m_InputValues->GroupByFeature) - { - header += " Phase " + StringUtilities::number(value); - } +Result<> MultiWriteOutStl(const fs::path& path, const IGeometry::MeshIndexType numTriangles, const std::string&& header, const IGeometry::MeshIndexArrayType& triangles, const Float32Array& vertices, + const Int32Array& featureIds, const int32 featureId) +{ + Result<> result; + // Create output file writer in binary write out mode to ensure cross-compatibility + FILE* filePtr = fopen(path.string().c_str(), "wb"); + if(filePtr == nullptr) + { + fclose(filePtr); + return {MakeWarningVoidResult(-27876, fmt::format("Error Opening STL File. Unable to create temp file at path '{}' for original file '{}'", path.string(), path.filename().string()))}; + } + + int32 triCount = 0; + + { // Scope header output processing to keep overhead low and increase readability if(header.size() >= 80) { - fclose(f); - atomicFile.setAutoCommit(false); // Set this to false otherwise - atomicFile.removeTempFile(); // Remove the temp file - return {MakeWarningVoidResult(-27874, fmt::format("Error Writing STL File '{}'. Header was over the 80 characters supported by STL. Length of header: {}.", filename, header.length()))}; + result = MakeWarningVoidResult(-27874, + fmt::format("Warning: Writing STL File '{}'. Header was over the 80 characters supported by STL. Length of header: {}. Only the first 80 bytes will be written.", + path.filename().string(), header.length())); } - char h[80]; + std::array stlFileHeader = {}; + stlFileHeader.fill(0); size_t headLength = 80; if(header.length() < 80) { headLength = static_cast(header.length()); } - std::string c_str = header; - ::memset(h, 0, 80); - ::memcpy(h, c_str.data(), headLength); + // std::string c_str = header; + memcpy(stlFileHeader.data(), header.data(), headLength); // Return the number of bytes written - which should be 80 - fwrite(h, 1, 80, f); - fwrite(&triCount, 1, 4, f); - triCount = 0; // Reset this to Zero. Increment for every triangle written + fwrite(stlFileHeader.data(), 1, 80, filePtr); + } + + fwrite(&triCount, 1, 4, filePtr); + triCount = 0; // Reset this to Zero. Increment for every triangle written + + size_t totalWritten = 0; + std::array vecA = {0.0f, 0.0f, 0.0f}; + std::array vecB = {0.0f, 0.0f, 0.0f}; + + std::array data = {}; + nonstd::span normalPtr(reinterpret_cast(data.data()), 3); + nonstd::span vert1Ptr(reinterpret_cast(data.data() + 12), 3); + nonstd::span vert2Ptr(reinterpret_cast(data.data() + 24), 3); + nonstd::span vert3Ptr(reinterpret_cast(data.data() + 36), 3); + nonstd::span attrByteCountPtr(reinterpret_cast(data.data() + 48), 2); + attrByteCountPtr[0] = 0; + + // Loop over all the triangles for this spin + for(IGeometry::MeshIndexType triangle = 0; triangle < numTriangles; ++triangle) + { + // Get the true indices of the 3 nodes + IGeometry::MeshIndexType nId0 = triangles[triangle * 3]; + IGeometry::MeshIndexType nId1 = triangles[triangle * 3 + 1]; + IGeometry::MeshIndexType nId2 = triangles[triangle * 3 + 2]; - // Loop over all the triangles for this spin - for(IGeometry::MeshIndexType t = 0; t < nTriangles; ++t) + if(featureIds[triangle * 2] == featureId) { - // Get the true indices of the 3 nodes - IGeometry::MeshIndexType nId0 = triangles[t * 3]; - IGeometry::MeshIndexType nId1 = triangles[t * 3 + 1]; - IGeometry::MeshIndexType nId2 = triangles[t * 3 + 2]; + // winding = 0; // 0 = Write it using forward spin + } + else if(featureIds[triangle * 2 + 1] == featureId) + { + // winding = 1; // Write it using backward spin + // Switch the 2 node indices + IGeometry::MeshIndexType temp = nId1; + nId1 = nId2; + nId2 = temp; + } + else + { + continue; // We do not match either spin so move to the next triangle + } - vert1[0] = static_cast(vertices[nId0 * 3]); - vert1[1] = static_cast(vertices[nId0 * 3 + 1]); - vert1[2] = static_cast(vertices[nId0 * 3 + 2]); + vert1Ptr[0] = static_cast(vertices[nId0 * 3]); + vert1Ptr[1] = static_cast(vertices[nId0 * 3 + 1]); + vert1Ptr[2] = static_cast(vertices[nId0 * 3 + 2]); - if(featureIds[t * 2] == featureId) - { - // winding = 0; // 0 = Write it using forward spin - } - else if(featureIds[t * 2 + 1] == featureId) + vert2Ptr[0] = static_cast(vertices[nId1 * 3]); + vert2Ptr[1] = static_cast(vertices[nId1 * 3 + 1]); + vert2Ptr[2] = static_cast(vertices[nId1 * 3 + 2]); + + vert3Ptr[0] = static_cast(vertices[nId2 * 3]); + vert3Ptr[1] = static_cast(vertices[nId2 * 3 + 1]); + vert3Ptr[2] = static_cast(vertices[nId2 * 3 + 2]); + + // Compute the normal + vecA[0] = vert2Ptr[0] - vert1Ptr[0]; + vecA[1] = vert2Ptr[1] - vert1Ptr[1]; + vecA[2] = vert2Ptr[2] - vert1Ptr[2]; + + vecB[0] = vert3Ptr[0] - vert1Ptr[0]; + vecB[1] = vert3Ptr[1] - vert1Ptr[1]; + vecB[2] = vert3Ptr[2] - vert1Ptr[2]; + + MatrixMath::CrossProduct(vecA.data(), vecB.data(), normalPtr.data()); + MatrixMath::Normalize3x1(normalPtr.data()); + + totalWritten = fwrite(data.data(), 1, 50, filePtr); + if(totalWritten != 50) + { + fclose(filePtr); + return {MakeWarningVoidResult( + -27873, fmt::format("Error Writing STL File '{}'. Not enough elements written for Feature Id {}. Wrote {} of 50. No file written.", path.filename().string(), featureId, totalWritten))}; + } + triCount++; + } + + fseek(filePtr, 80L, SEEK_SET); + fwrite(reinterpret_cast(&triCount), 1, 4, filePtr); + fclose(filePtr); + return result; +} +} // namespace + +// ----------------------------------------------------------------------------- +WriteStlFile::WriteStlFile(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, WriteStlFileInputValues* inputValues) +: m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(mesgHandler) +{ +} + +// ----------------------------------------------------------------------------- +WriteStlFile::~WriteStlFile() noexcept = default; + +// ----------------------------------------------------------------------------- +const std::atomic_bool& WriteStlFile::getCancel() +{ + return m_ShouldCancel; +} + +// ----------------------------------------------------------------------------- +Result<> WriteStlFile::operator()() +{ + const auto& triangleGeom = m_DataStructure.getDataRefAs(m_InputValues->TriangleGeomPath); + const Float32Array& vertices = triangleGeom.getVerticesRef(); + const IGeometry::MeshIndexArrayType& triangles = triangleGeom.getFacesRef(); + const IGeometry::MeshIndexType nTriangles = triangleGeom.getNumberOfFaces(); + + auto groupingType = static_cast(m_InputValues->GroupingType); + + if(groupingType == GroupingType::None) + { + AtomicFile atomicFile(m_InputValues->OutputStlFile.string(), false); + + if(atomicFile.getResult().invalid()) + { + return atomicFile.getResult(); + } + + { // Scoped to ensure file lock is released and header string is untouched since it is invalid after move + std::string header = "DREAM3D Generated For Triangle Geom"; // Char count: 35 + + // validate name is less than 40 characters + if(triangleGeom.getName().size() < 41) { - // winding = 1; // Write it using backward spin - // Switch the 2 node indices - IGeometry::MeshIndexType temp = nId1; - nId1 = nId2; - nId2 = temp; + header += " " + triangleGeom.getName(); } - else + + auto result = ::SingleWriteOutStl(atomicFile.tempFilePath(), nTriangles, std::move(header), triangles, vertices); + if(result.invalid()) { - continue; // We do not match either spin so move to the next triangle + return result; } + } - vert2[0] = static_cast(vertices[nId1 * 3]); - vert2[1] = static_cast(vertices[nId1 * 3 + 1]); - vert2[2] = static_cast(vertices[nId1 * 3 + 2]); + atomicFile.commit(); + return {}; + } - vert3[0] = static_cast(vertices[nId2 * 3]); - vert3[1] = static_cast(vertices[nId2 * 3 + 1]); - vert3[2] = static_cast(vertices[nId2 * 3 + 2]); + const std::filesystem::path outputPath = m_InputValues->OutputStlDirectory; + { // Scope to cut overhead + // Make sure any directory path is also available as the user may have just typed + // in a path without actually creating the full path + Result<> createDirectoriesResult = nx::core::CreateOutputDirectories(outputPath); + if(createDirectoriesResult.invalid()) + { + return createDirectoriesResult; + } + } - // Compute the normal - u[0] = vert2[0] - vert1[0]; - u[1] = vert2[1] - vert1[1]; - u[2] = vert2[2] - vert1[2]; + // Store a list of Atomic Files, so we can clean up or finish depending on the outcome of all the writes + std::vector> fileList = {}; - w[0] = vert3[0] - vert1[0]; - w[1] = vert3[1] - vert1[1]; - w[2] = vert3[2] - vert1[2]; + { // Scope to cut overhead and ensure file lock is released on windows + const auto& featureIds = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsPath); + // Store all the unique Spins + if(groupingType == GroupingType::Features) + { + // Faster and more memory efficient since we don't need phases + std::unordered_set uniqueGrainIds(featureIds.cbegin(), featureIds.cend()); - normal[0] = u[1] * w[2] - u[2] * w[1]; - normal[1] = u[2] * w[0] - u[0] * w[2]; - normal[2] = u[0] * w[1] - u[1] * w[0]; + fileList.reserve(uniqueGrainIds.size()); - length = sqrtf(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]); - normal[0] = normal[0] / length; - normal[1] = normal[1] / length; - normal[2] = normal[2] / length; + usize fileIndex = 0; + for(const auto featureId : uniqueGrainIds) + { + // Generate the output file + fileList.push_back( + std::make_unique(m_InputValues->OutputStlDirectory.string() + "/" + m_InputValues->OutputStlPrefix + "Feature_" + StringUtilities::number(featureId) + ".stl", false)); + + if(fileList[fileIndex]->getResult().invalid()) + { + return fileList[fileIndex]->getResult(); + } + + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing STL for Feature Id {}", featureId)); + + auto result = + ::MultiWriteOutStl(fileList[fileIndex]->tempFilePath(), nTriangles, {"DREAM3D Generated For Feature ID " + StringUtilities::number(featureId)}, triangles, vertices, featureIds, featureId); + // if valid Loop over all the triangles for this spin + if(result.invalid()) + { + return result; + } + + fileIndex++; + } + } + if(groupingType == GroupingType::FeaturesAndPhases) + { + std::map uniqueGrainIdToPhase; + + const auto& featurePhases = m_DataStructure.getDataRefAs(m_InputValues->FeaturePhasesPath); + for(IGeometry::MeshIndexType i = 0; i < nTriangles; i++) + { + uniqueGrainIdToPhase.emplace(featureIds[i * 2], featurePhases[i * 2]); + uniqueGrainIdToPhase.emplace(featureIds[i * 2 + 1], featurePhases[i * 2 + 1]); + } - totalWritten = fwrite(data, 1, 50, f); - if(totalWritten != 50) + // Loop over the unique feature Ids + usize fileIndex = 0; + for(const auto& [featureId, value] : uniqueGrainIdToPhase) { - fclose(f); - atomicFile.setAutoCommit(false); // Set this to false otherwise - atomicFile.removeTempFile(); // Remove the temp file - return {MakeWarningVoidResult(-27873, - fmt::format("Error Writing STL File '{}'. Not enough elements written for Feature Id {}. Wrote {} of 50. No file written.", filename, featureId, totalWritten))}; + // Generate the output file + fileList.push_back(std::make_unique(m_InputValues->OutputStlDirectory.string() + "/" + m_InputValues->OutputStlPrefix + "Ensemble_" + StringUtilities::number(value) + "_" + + "Feature_" + StringUtilities::number(featureId) + ".stl", + false)); + + if(fileList[fileIndex]->getResult().invalid()) + { + return fileList[fileIndex]->getResult(); + } + + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing STL for Feature Id {}", featureId)); + + auto result = + ::MultiWriteOutStl(fileList[fileIndex]->tempFilePath(), nTriangles, {"DREAM3D Generated For Feature ID " + StringUtilities::number(featureId) + " Phase " + StringUtilities::number(value)}, + triangles, vertices, featureIds, featureId); + // if valid loop over all the triangles for this spin + if(result.invalid()) + { + return result; + } + + fileIndex++; } - triCount++; } + } - fseek(f, 80L, SEEK_SET); - fwrite(reinterpret_cast(&triCount), 1, 4, f); - fclose(f); + for(const auto& atomicFile : fileList) + { + atomicFile->commit(); } return {}; } + +// NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast, cppcoreguidelines-pro-bounds-pointer-arithmetic) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.hpp index 489c3c0efa..c333b537f5 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.hpp @@ -6,21 +6,29 @@ #include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/Filter/IFilter.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" namespace nx::core { +enum class GroupingType : ChoicesParameter::ValueType +{ + Features, + FeaturesAndPhases, + None +}; + struct SIMPLNXCORE_EXPORT WriteStlFileInputValues { - bool GroupByFeature; + ChoicesParameter::ValueType GroupingType; + FileSystemPathParameter::ValueType OutputStlFile; FileSystemPathParameter::ValueType OutputStlDirectory; StringParameter::ValueType OutputStlPrefix; DataPath FeatureIdsPath; DataPath FeaturePhasesPath; DataPath TriangleGeomPath; - // DataPath FaceNormalsPath; }; /** diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.cpp index 1b423f1fc7..6e73eb4542 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.cpp @@ -6,7 +6,7 @@ #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Filter/Actions/EmptyAction.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" -#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" @@ -57,24 +57,38 @@ Parameters WriteStlFileFilter::parameters() const // Create the parameter descriptors that are needed for this filter params.insertSeparator(Parameters::Separator{"Input Parameters"}); - params.insertLinkableParameter(std::make_unique(k_GroupByFeature, "Group by Feature Phases", "Further partition the stl files by feature phases", false)); + params.insertLinkableParameter(std::make_unique(k_GroupingType_Key, "File Grouping Type", "How to partition the stl files", to_underlying(GroupingType::Features), + ChoicesParameter::Choices{"Features", "Phases and Features", "None [Single File]"})); // sequence dependent DO NOT REORDER params.insert(std::make_unique(k_OutputStlDirectory_Key, "Output STL Directory", "Directory to dump the STL file(s) to", fs::path(), FileSystemPathParameter::ExtensionsType{}, FileSystemPathParameter::PathType::OutputDir, true)); params.insert( std::make_unique(k_OutputStlPrefix_Key, "STL File Prefix", "The prefix name of created files (other values will be appended later - including the .stl extension)", "Triangle")); + params.insert(std::make_unique(k_OutputStlFile_Key, "Output STL File", "STL File to dump the Triangle Geometry to", fs::path(), + FileSystemPathParameter::ExtensionsType{".stl"}, FileSystemPathParameter::PathType::OutputFile, false)); + params.insertSeparator(Parameters::Separator{"Required Data Objects"}); params.insert(std::make_unique(k_TriangleGeomPath_Key, "Selected Triangle Geometry", "The geometry to print", DataPath{}, GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Triangle})); params.insert(std::make_unique(k_FeatureIdsPath_Key, "Face labels", "The triangle feature ids array to order/index files by", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{2}})); - // params.insert(std::make_unique(k_FaceNormalsPath_Key, "Face Normals", "The triangle normals array to be printed in the stl file", DataPath{}, - // ArraySelectionParameter::AllowedTypes{DataType::float32}, ArraySelectionParameter::AllowedComponentShapes{{3}})); params.insert(std::make_unique(k_FeaturePhasesPath_Key, "Feature Phases", "The feature phases array to further order/index files by", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); - // link params - params.linkParameters(k_GroupByFeature, k_FeaturePhasesPath_Key, true); + // link params -- GroupingType enum is stored in the algorithm header [WriteStlFile.hpp] + //------------ Group by Features ------------- + params.linkParameters(k_GroupingType_Key, k_OutputStlDirectory_Key, to_underlying(GroupingType::Features)); + params.linkParameters(k_GroupingType_Key, k_OutputStlPrefix_Key, to_underlying(GroupingType::Features)); + params.linkParameters(k_GroupingType_Key, k_FeatureIdsPath_Key, to_underlying(GroupingType::Features)); + + //------- Group by Features and Phases ------- + params.linkParameters(k_GroupingType_Key, k_OutputStlDirectory_Key, to_underlying(GroupingType::FeaturesAndPhases)); + params.linkParameters(k_GroupingType_Key, k_OutputStlPrefix_Key, to_underlying(GroupingType::FeaturesAndPhases)); + params.linkParameters(k_GroupingType_Key, k_FeatureIdsPath_Key, to_underlying(GroupingType::FeaturesAndPhases)); + params.linkParameters(k_GroupingType_Key, k_FeaturePhasesPath_Key, to_underlying(GroupingType::FeaturesAndPhases)); + + //--------------- Single File ---------------- + params.linkParameters(k_GroupingType_Key, k_OutputStlFile_Key, to_underlying(GroupingType::None)); return params; } @@ -89,12 +103,11 @@ IFilter::UniquePointer WriteStlFileFilter::clone() const IFilter::PreflightResult WriteStlFileFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) const { - auto pGroupByPhasesValue = filterArgs.value(k_GroupByFeature); + auto pGroupingTypeValue = static_cast(filterArgs.value(k_GroupingType_Key)); auto pOutputStlDirectoryValue = filterArgs.value(k_OutputStlDirectory_Key); auto pTriangleGeomPathValue = filterArgs.value(k_TriangleGeomPath_Key); auto pFeatureIdsPathValue = filterArgs.value(k_FeatureIdsPath_Key); auto pFeaturePhasesPathValue = filterArgs.value(k_FeaturePhasesPath_Key); - // auto pFaceNormalsPathValue = filterArgs.value(k_FaceNormalsPath_Key); PreflightResult preflightResult; nx::core::Result resultOutputActions; @@ -112,17 +125,19 @@ IFilter::PreflightResult WriteStlFileFilter::preflightImpl(const DataStructure& -27871, fmt::format("The number of triangles is {}, but the STL specification only supports triangle counts up to {}", triangleGeom->getNumberOfFaces(), std::numeric_limits::max())); } - if(pGroupByPhasesValue) + if(pGroupingTypeValue == GroupingType::FeaturesAndPhases) { if(auto* featurePhases = dataStructure.getDataAs(pFeaturePhasesPathValue); featurePhases == nullptr) { return MakePreflightErrorResult(-27872, fmt::format("Feature Phases Array doesn't exist at: {}", pFeaturePhasesPathValue.toString())); } } - - if(auto* featureIds = dataStructure.getDataAs(pFeatureIdsPathValue); featureIds == nullptr) + if(pGroupingTypeValue != GroupingType::None) { - return MakePreflightErrorResult(-27873, fmt::format("Feature Ids Array doesn't exist at: {}", pFeatureIdsPathValue.toString())); + if(auto* featureIds = dataStructure.getDataAs(pFeatureIdsPathValue); featureIds == nullptr) + { + return MakePreflightErrorResult(-27873, fmt::format("Feature Ids Array doesn't exist at: {}", pFeatureIdsPathValue.toString())); + } } // Return both the resultOutputActions and the preflightUpdatedValues via std::move() @@ -135,13 +150,13 @@ Result<> WriteStlFileFilter::executeImpl(DataStructure& dataStructure, const Arg { WriteStlFileInputValues inputValues; - inputValues.GroupByFeature = filterArgs.value(k_GroupByFeature); + inputValues.GroupingType = filterArgs.value(k_GroupingType_Key); + inputValues.OutputStlFile = filterArgs.value(k_OutputStlFile_Key); inputValues.OutputStlDirectory = filterArgs.value(k_OutputStlDirectory_Key); inputValues.OutputStlPrefix = filterArgs.value(k_OutputStlPrefix_Key); inputValues.FeatureIdsPath = filterArgs.value(k_FeatureIdsPath_Key); inputValues.FeaturePhasesPath = filterArgs.value(k_FeaturePhasesPath_Key); inputValues.TriangleGeomPath = filterArgs.value(k_TriangleGeomPath_Key); - // inputValues.FaceNormalsPath = filterArgs.value(k_FaceNormalsPath_Key); return WriteStlFile(dataStructure, messageHandler, shouldCancel, &inputValues)(); } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.hpp index 2c28da6aee..b53cc231fc 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.hpp @@ -24,13 +24,13 @@ class SIMPLNXCORE_EXPORT WriteStlFileFilter : public IFilter WriteStlFileFilter& operator=(WriteStlFileFilter&&) noexcept = delete; // Parameter Keys - static inline constexpr StringLiteral k_GroupByFeature = "group_by_feature"; + static inline constexpr StringLiteral k_GroupingType_Key = "grouping_type"; + static inline constexpr StringLiteral k_OutputStlFile_Key = "output_stl_file"; static inline constexpr StringLiteral k_OutputStlDirectory_Key = "output_stl_directory"; static inline constexpr StringLiteral k_OutputStlPrefix_Key = "output_stl_prefix"; static inline constexpr StringLiteral k_FeatureIdsPath_Key = "feature_ids_path"; static inline constexpr StringLiteral k_FeaturePhasesPath_Key = "feature_phases_path"; static inline constexpr StringLiteral k_TriangleGeomPath_Key = "triangle_geom_path"; - // static inline constexpr StringLiteral k_FaceNormalsPath_Key = "face_normals_path"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index 4b0a405dd7..43b00dbfe2 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -220,6 +220,7 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME k_files.tar.gz SHA512 2701d7bca711821df78bf7649f9b5928cd9febeb4f04e5c8ce8008633304128e3e9290dadfb01e15f73b281a55c6d65255ca4bd115bf04a3218c32dfd7337d7a) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME SurfaceMeshTest.tar.gz SHA512 a74e9fa40ccec78174dbbac90969bfa17367c3a7e14b7b84624032317c351de89957750803d7cb43c67dec19281ee4f647de022d474566fd43e25b8230cce6d6) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME initialize_data_test_files.tar.gz SHA512 f04fe76ef96add4b775111ae7fc95233a7748a25501d9f00a8b2a162c87782b8cd2813e6e46ba7892e721976693da06965e624335dbb28224c9c5b877a05aa49) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME 6_6_write_stl_file_test.tar.gz SHA512 05455dfb57724a14a3b2d92ad02ac335b5596ca4f353830ee60d84ffc858523136140328a1c95b6693331be9dc5c40a26e76f45ee54ab11e218f5efb6b2a4c38) endif() diff --git a/src/Plugins/SimplnxCore/test/WriteStlFileTest.cpp b/src/Plugins/SimplnxCore/test/WriteStlFileTest.cpp index 1c731b5ddb..6a68c31990 100644 --- a/src/Plugins/SimplnxCore/test/WriteStlFileTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteStlFileTest.cpp @@ -4,6 +4,7 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" +#include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" @@ -15,7 +16,7 @@ using namespace nx::core; namespace { -const std::string k_ExemplarDir = fmt::format("{}/6_6_write_stl_test", unit_test::k_TestFilesDir); +const std::string k_ExemplarDir = fmt::format("{}/6_6_write_stl_file_test", unit_test::k_TestFilesDir); std::vector readIn(fs::path filePath) { @@ -38,7 +39,7 @@ std::vector readIn(fs::path filePath) return {}; } -void CompareResults() // compare hash of both file strings +void CompareMultipleResults() // compare hash of both file strings { fs::path writtenFilePath = fs::path(std::string(unit_test::k_BinaryTestOutputDir) + "/TriangleFeature_0.stl"); REQUIRE(fs::exists(writtenFilePath)); @@ -51,11 +52,20 @@ void CompareResults() // compare hash of both file strings REQUIRE(fs::exists(exemplarFilePath2)); REQUIRE(readIn(writtenFilePath2) == readIn(exemplarFilePath2)); } + +void CompareSingleResult() // compare hash of both file strings +{ + fs::path writtenFilePath = fs::path(std::string(unit_test::k_BinaryTestOutputDir) + "/Generated.stl"); + REQUIRE(fs::exists(writtenFilePath)); + fs::path exemplarFilePath = fs::path(k_ExemplarDir + "/Exemplar.stl"); + REQUIRE(fs::exists(exemplarFilePath)); + REQUIRE(readIn(writtenFilePath) == readIn(exemplarFilePath)); +} } // namespace -TEST_CASE("SimplnxCore::WriteStlFileFilter: Valid Filter Execution", "[SimplnxCore][WriteStlFileFilter]") +TEST_CASE("SimplnxCore::WriteStlFileFilter: Multiple File Valid", "[SimplnxCore][WriteStlFileFilter]") { - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_CMakeExecutable, nx::core::unit_test::k_TestFilesDir, "6_6_write_stl_test.tar.gz", "6_6_write_stl_test"); + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_CMakeExecutable, nx::core::unit_test::k_TestFilesDir, "6_6_write_stl_file_test.tar.gz", "6_6_write_stl_file_test"); // Instantiate the filter, a DataStructure object and an Arguments Object WriteStlFileFilter filter; @@ -64,7 +74,7 @@ TEST_CASE("SimplnxCore::WriteStlFileFilter: Valid Filter Execution", "[SimplnxCo Arguments args; // Create default Parameters for the filter. - args.insertOrAssign(WriteStlFileFilter::k_GroupByFeature, std::make_any(false)); + args.insertOrAssign(WriteStlFileFilter::k_GroupingType_Key, std::make_any(0)); args.insertOrAssign(WriteStlFileFilter::k_OutputStlDirectory_Key, std::make_any(fs::path(std::string(unit_test::k_BinaryTestOutputDir)))); args.insertOrAssign(WriteStlFileFilter::k_OutputStlPrefix_Key, std::make_any("Triangle")); args.insertOrAssign(WriteStlFileFilter::k_TriangleGeomPath_Key, std::make_any(DataPath({"TriangleDataContainer"}))); @@ -78,5 +88,31 @@ TEST_CASE("SimplnxCore::WriteStlFileFilter: Valid Filter Execution", "[SimplnxCo auto executeResult = filter.execute(dataStructure, args); REQUIRE(executeResult.result.valid()); - ::CompareResults(); + ::CompareMultipleResults(); +} + +TEST_CASE("SimplnxCore::WriteStlFileFilter: Single File Valid", "[SimplnxCore][WriteStlFileFilter]") +{ + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_CMakeExecutable, nx::core::unit_test::k_TestFilesDir, "6_6_write_stl_file_test.tar.gz", "6_6_write_stl_file_test"); + + // Instantiate the filter, a DataStructure object and an Arguments Object + WriteStlFileFilter filter; + auto exemplarFilePath = fs::path(fmt::format("{}/exemplar.dream3d", k_ExemplarDir)); + DataStructure dataStructure = UnitTest::LoadDataStructure(exemplarFilePath); + Arguments args; + + // Create default Parameters for the filter. + args.insertOrAssign(WriteStlFileFilter::k_GroupingType_Key, std::make_any(2)); + args.insertOrAssign(WriteStlFileFilter::k_OutputStlFile_Key, std::make_any(fs::path(std::string(unit_test::k_BinaryTestOutputDir) + "/Generated.stl"))); + args.insertOrAssign(WriteStlFileFilter::k_TriangleGeomPath_Key, std::make_any(DataPath({"TriangleDataContainer"}))); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + REQUIRE(preflightResult.outputActions.valid()); + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + REQUIRE(executeResult.result.valid()); + + ::CompareSingleResult(); }