diff --git a/CMakeLists.txt b/CMakeLists.txt index 35cb04f916..221218bc73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -548,6 +548,7 @@ set(SIMPLNX_HDRS ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/HDF5/Writers/ObjectWriter.hpp ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/Text/CsvParser.hpp + ${SIMPLNX_SOURCE_DIR}/Utilities/MD5.hpp ) set(SIMPLNX_GENERATED_HEADERS @@ -743,6 +744,7 @@ set(SIMPLNX_SRCS ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/HDF5/Writers/ObjectWriter.cpp ${SIMPLNX_SOURCE_DIR}/Utilities/Parsing/Text/CsvParser.cpp + ${SIMPLNX_SOURCE_DIR}/Utilities/MD5.cpp ) # Add Core FilterParameters diff --git a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt index 89af617cb7..c498452f8a 100644 --- a/src/Plugins/ITKImageProcessing/test/CMakeLists.txt +++ b/src/Plugins/ITKImageProcessing/test/CMakeLists.txt @@ -98,8 +98,6 @@ target_sources(${PLUGIN_NAME}UnitTest PRIVATE ${${PLUGIN_NAME}_SOURCE_DIR}/test/ITKTestBase.hpp ${${PLUGIN_NAME}_SOURCE_DIR}/test/ITKTestBase.cpp - ${${PLUGIN_NAME}_SOURCE_DIR}/test/MD5.hpp - ${${PLUGIN_NAME}_SOURCE_DIR}/test/MD5.cpp ) # ----------------------------------------------------------------------------- diff --git a/src/Plugins/ITKImageProcessing/test/ITKTestBase.cpp b/src/Plugins/ITKImageProcessing/test/ITKTestBase.cpp index 74adcb583f..b448ea2c7a 100644 --- a/src/Plugins/ITKImageProcessing/test/ITKTestBase.cpp +++ b/src/Plugins/ITKImageProcessing/test/ITKTestBase.cpp @@ -2,7 +2,6 @@ #include "ITKImageProcessing/Filters/ITKImageReaderFilter.hpp" #include "ITKImageProcessing/Filters/ITKImageWriterFilter.hpp" -#include "MD5.hpp" #include #include @@ -12,6 +11,7 @@ #include #include "simplnx/Common/Types.hpp" +#include "simplnx/Utilities/MD5.hpp" #include diff --git a/src/Plugins/SimplnxCore/docs/CombineStlFilesFilter.md b/src/Plugins/SimplnxCore/docs/CombineStlFilesFilter.md index e7e0f541b7..a951a6ef42 100644 --- a/src/Plugins/SimplnxCore/docs/CombineStlFilesFilter.md +++ b/src/Plugins/SimplnxCore/docs/CombineStlFilesFilter.md @@ -2,13 +2,16 @@ ## Group (Subgroup) -Reader/Input (AMProcessMonitoring) +Reader/Input ## Description This **Filter** combines all of the STL files from a given directory into a single triangle geometry. This filter will make use of the **Import STL File Filter** to read in each stl file in the given directory and then will proceed to combine each of the imported files into a single triangle geometry. -There is an option to label the triangles and vertices with the "index" of the file that was read. This would be based on the lexographical index and starts from 1. This allows for the immediate "segmentation" of the resulting triangle geometry or just as a convenience to "color by" in the visualization widget. +There is an option to label the faces and vertices with a "Part Number" that represents the index into the list of files that was used as the input. This would be based on the lexographical index and starts from 1. This allows for the immediate "segmentation" of the resulting triangle geometry or just as a convenience to "color by" in the visualization widget. This can +also be used in the "Write STL Files from Triangle Geometry" Filter if the selection for +the "File Grouping Type" is set to "Part Index" in the UI. Then use the "Part Number" array +that is created in this filter for the "Part Index". % Auto generated parameter table will be inserted here diff --git a/src/Plugins/SimplnxCore/docs/WriteStlFileFilter.md b/src/Plugins/SimplnxCore/docs/WriteStlFileFilter.md index 0d3afa724d..0275de0e86 100644 --- a/src/Plugins/SimplnxCore/docs/WriteStlFileFilter.md +++ b/src/Plugins/SimplnxCore/docs/WriteStlFileFilter.md @@ -6,7 +6,20 @@ IO (Output) ## Description -This **Filter** will write a binary STL File for each unique **Feature** Id in the associated **Triangle** geometry. The STL files will be named with the [Feature_Id].stl. The user can designate an optional prefix for the files. +This **Filter** can generate a single or multiple binary STL (StereoLithography) +files for a given Triangle Geometry. This is controlled by the "File Grouping Type" +parameter which is a combo box parameter type with the following choices: + +- Features: The user must supply a 2 component Int32 array where the values are the 2 features that the triangle belongs to. +- Phases and Features: The user must the same kind of array as in the "Features" but also + must supply a single component array that represents another higher order of grouping, such + as a phase (in the case of microstructure data) or a part number in the base of importing + a large multipart geometry. +- Single File: The entire Triangle Geometry is saved as a single STL File +- Part Index: The user must supply a single component Int32 array at the triangle + level that represents which part the triangle belongs to. This is handy when the Triangle Geometry + has distinct, non-overlapping "parts", such as an AM build with. + % Auto generated parameter table will be inserted here diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CombineStlFiles.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CombineStlFiles.cpp index eab5207a2f..9b38f1e6b1 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CombineStlFiles.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CombineStlFiles.cpp @@ -82,25 +82,52 @@ const std::atomic_bool& CombineStlFiles::getCancel() Result<> CombineStlFiles::operator()() { DataStructure tempDataStructure; - for(const auto& dirEntry : std::filesystem::directory_iterator{m_InputValues->StlFilesPath}) + std::vector paths; + const std::string ext(".stl"); + + // Just count up the stl files in the directory + size_t index = 0; + for(const auto& entry : std::filesystem::directory_iterator{m_InputValues->StlFilesPath}) + { + if(fs::is_regular_file(entry) && StringUtilities::toLower(entry.path().extension().string()) == ext) + { + paths.emplace_back(entry); + } + } + + // Sort the paths for something sort of reasonable. + std::sort(paths.begin(), paths.end()); + + auto pCellFeatureAttributeMatrixPath = m_InputValues->TriangleDataContainerName.createChildPath(m_InputValues->CellFeatureAttributeMatrixName); + auto activeArrayPath = pCellFeatureAttributeMatrixPath.createChildPath(m_InputValues->ActiveArrayName); + auto fileListPath = pCellFeatureAttributeMatrixPath.createChildPath(m_InputValues->FileListArrayName); + + auto activeArray = m_DataStructure.getDataRefAs(activeArrayPath); + activeArray[0] = 0; + auto fileListStrArray = m_DataStructure.getDataRefAs(fileListPath); + + int32 currentIndex = 1; + for(const auto& filePath : paths) { + std::string stlFilePath = filePath.string(); if(getCancel()) { return {}; } - const fs::path& stlFilePath = dirEntry.path(); - if(fs::is_regular_file(stlFilePath) && StringUtilities::toLower(stlFilePath.extension().string()) == ".stl") + fileListStrArray[currentIndex] = stlFilePath; + activeArray[currentIndex] = 1; + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("({}/{}) Reading {}", currentIndex, paths.size(), stlFilePath)); + currentIndex++; + + ReadStlFileFilter stlFileReader; + Arguments args; + args.insertOrAssign(ReadStlFileFilter::k_StlFilePath_Key, std::make_any(stlFilePath)); + args.insertOrAssign(ReadStlFileFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(DataPath({filePath.stem().string()}))); + auto executeResult = stlFileReader.execute(tempDataStructure, args); + if(executeResult.result.invalid()) { - ReadStlFileFilter stlFileReader; - Arguments args; - args.insertOrAssign(ReadStlFileFilter::k_StlFilePath_Key, std::make_any(stlFilePath)); - args.insertOrAssign(ReadStlFileFilter::k_CreatedTriangleGeometryPath_Key, std::make_any(DataPath({stlFilePath.stem().string()}))); - auto executeResult = stlFileReader.execute(tempDataStructure, args); - if(executeResult.result.invalid()) - { - return executeResult.result; - } + return executeResult.result; } } @@ -144,6 +171,8 @@ Result<> CombineStlFiles::operator()() usize faceLabelOffset = 0; usize vertexLabelOffset = 0; + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Moving final triangle geometry data...")); + // Loop over each temp geometry and copy the data into the destination geometry for(auto* currentGeometry : stlGeometries) { @@ -167,11 +196,7 @@ Result<> CombineStlFiles::operator()() if(m_InputValues->LabelFaces) { - auto& faceLabels = m_DataStructure.getDataRefAs(m_InputValues->FaceFileIndexArrayPath); - // for(usize tuple = faceLabelOffset; tuple < faceLabelOffset + currentGeomNumTriangles; tuple++) - // { - // faceLabels[tuple] = fileIndex; - // } + auto& faceLabels = m_DataStructure.getDataRefAs(m_InputValues->FaceFileIndexArrayPath); std::fill(faceLabels.begin() + faceLabelOffset, faceLabels.begin() + faceLabelOffset + currentGeomNumTriangles, fileIndex); } @@ -179,11 +204,7 @@ Result<> CombineStlFiles::operator()() if(m_InputValues->LabelVertices) { - auto& vertexLabels = m_DataStructure.getDataRefAs(m_InputValues->VertexFileIndexArrayPath); - // for(usize tuple = vertexLabelOffset; tuple < vertexLabelOffset + currentGeomNumVertices; tuple++) - // { - // vertexLabels[tuple] = fileIndex; - // } + auto& vertexLabels = m_DataStructure.getDataRefAs(m_InputValues->VertexFileIndexArrayPath); std::fill(vertexLabels.begin() + vertexLabelOffset, vertexLabels.begin() + vertexLabelOffset + currentGeomNumVertices, fileIndex); } vertexLabelOffset += currentGeomNumVertices; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CombineStlFiles.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CombineStlFiles.hpp index 769e00c586..5c55bb0c5b 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CombineStlFiles.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CombineStlFiles.hpp @@ -5,6 +5,7 @@ #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/Filter/IFilter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" @@ -21,6 +22,9 @@ struct SIMPLNXCORE_EXPORT CombineStlFilesInputValues bool LabelFaces; DataPath VertexFileIndexArrayPath; bool LabelVertices; + DataObjectNameParameter::ValueType CellFeatureAttributeMatrixName; + DataObjectNameParameter::ValueType ActiveArrayName; + DataObjectNameParameter::ValueType FileListArrayName; }; /** diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.cpp index f82ae19a79..efe418a18c 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.cpp @@ -6,6 +6,9 @@ #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/Math/MatrixMath.hpp" +#include "simplnx/Utilities/ParallelAlgorithmUtilities.hpp" +#include "simplnx/Utilities/ParallelDataAlgorithm.hpp" +#include "simplnx/Utilities/ParallelTaskAlgorithm.hpp" #include "simplnx/Utilities/StringUtilities.hpp" #include @@ -116,6 +119,17 @@ Result<> SingleWriteOutStl(const fs::path& path, const IGeometry::MeshIndexType return result; } +/** + * @brief + * @param path + * @param numTriangles + * @param header + * @param triangles + * @param vertices + * @param featureIds + * @param featureId + * @return + */ 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) { @@ -168,6 +182,7 @@ Result<> MultiWriteOutStl(const fs::path& path, const IGeometry::MeshIndexType n nonstd::span attrByteCountPtr(reinterpret_cast(data.data() + 48), 2); attrByteCountPtr[0] = 0; + const usize numComps = featureIds.getNumberOfComponents(); // Loop over all the triangles for this spin for(IGeometry::MeshIndexType triangle = 0; triangle < numTriangles; ++triangle) { @@ -176,13 +191,12 @@ Result<> MultiWriteOutStl(const fs::path& path, const IGeometry::MeshIndexType n IGeometry::MeshIndexType nId1 = triangles[triangle * 3 + 1]; IGeometry::MeshIndexType nId2 = triangles[triangle * 3 + 2]; - if(featureIds[triangle * 2] == featureId) + if(featureIds[triangle * numComps] == featureId) { // winding = 0; // 0 = Write it using forward spin } - else if(featureIds[triangle * 2 + 1] == featureId) + else if(numComps > 1 && featureIds[triangle * numComps + 1] == featureId) { - // winding = 1; // Write it using backward spin // Switch the 2 node indices IGeometry::MeshIndexType temp = nId1; nId1 = nId2; @@ -222,7 +236,7 @@ Result<> MultiWriteOutStl(const fs::path& path, const IGeometry::MeshIndexType n { 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))}; + -27873, fmt::format("Error Writing STL File '{}': Not enough bytes written for triangle {}. Only {} bytes written of 50 bytes", path.filename().string(), triCount, totalWritten))}; } triCount++; } @@ -232,6 +246,154 @@ Result<> MultiWriteOutStl(const fs::path& path, const IGeometry::MeshIndexType n fclose(filePtr); return result; } + +/** + * @brief This class provides an interface to write the STL Files in parallel + */ +class MultiWriteStlFileImpl +{ +public: + MultiWriteStlFileImpl(WriteStlFile* filter, 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) + : m_Filter(filter) + , m_Path(path) + , m_NumTriangles(numTriangles) + , m_Header(header) + , m_Triangles(triangles) + , m_Vertices(vertices) + , m_FeatureIds(featureIds) + , m_FeatureId(featureId) + { + } + ~MultiWriteStlFileImpl() = default; + + void operator()() const + { + // Create output file writer in binary write out mode to ensure cross-compatibility + FILE* filePtr = fopen(m_Path.string().c_str(), "wb"); + + if(filePtr == nullptr) + { + fclose(filePtr); + m_Filter->sendThreadSafeProgressMessage( + {MakeWarningVoidResult(-27876, fmt::format("Error Opening STL File. Unable to create temp file at path '{}' for original file '{}'", m_Path.string(), m_Path.filename().string()))}); + return; + } + + int32 triCount = 0; + + { // Scope header output processing to keep overhead low and increase readability + if(m_Header.size() >= 80) + { + m_Filter->sendThreadSafeProgressMessage(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.", + m_Path.filename().string(), m_Header.length()))); + } + + std::array stlFileHeader = {}; + stlFileHeader.fill(0); + size_t headLength = 80; + if(m_Header.length() < 80) + { + headLength = static_cast(m_Header.length()); + } + + // std::string c_str = header; + memcpy(stlFileHeader.data(), m_Header.data(), headLength); + // Return the number of bytes written - which should be 80 + 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; + + const usize numComps = m_FeatureIds.getNumberOfComponents(); + // Loop over all the triangles for this spin + for(IGeometry::MeshIndexType triangle = 0; triangle < m_NumTriangles; ++triangle) + { + // Get the true indices of the 3 nodes + IGeometry::MeshIndexType nId0 = m_Triangles[triangle * 3]; + IGeometry::MeshIndexType nId1 = m_Triangles[triangle * 3 + 1]; + IGeometry::MeshIndexType nId2 = m_Triangles[triangle * 3 + 2]; + + if(m_FeatureIds[triangle * numComps] == m_FeatureId) + { + // winding = 0; // 0 = Write it using forward spin + } + else if(numComps > 1 && m_FeatureIds[triangle * numComps + 1] == m_FeatureId) + { + // 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 + } + + vert1Ptr[0] = static_cast(m_Vertices[nId0 * 3]); + vert1Ptr[1] = static_cast(m_Vertices[nId0 * 3 + 1]); + vert1Ptr[2] = static_cast(m_Vertices[nId0 * 3 + 2]); + + vert2Ptr[0] = static_cast(m_Vertices[nId1 * 3]); + vert2Ptr[1] = static_cast(m_Vertices[nId1 * 3 + 1]); + vert2Ptr[2] = static_cast(m_Vertices[nId1 * 3 + 2]); + + vert3Ptr[0] = static_cast(m_Vertices[nId2 * 3]); + vert3Ptr[1] = static_cast(m_Vertices[nId2 * 3 + 1]); + vert3Ptr[2] = static_cast(m_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); + m_Filter->sendThreadSafeProgressMessage({MakeWarningVoidResult( + -27873, fmt::format("Error Writing STL File '{}': Not enough bytes written for triangle {}. Only {} bytes written of 50 bytes", m_Path.filename().string(), triCount, totalWritten))}); + break; + } + triCount++; + } + + fseek(filePtr, 80L, SEEK_SET); + fwrite(reinterpret_cast(&triCount), 1, 4, filePtr); + fclose(filePtr); + } + +private: + WriteStlFile* m_Filter = nullptr; + const fs::path m_Path; + const IGeometry::MeshIndexType m_NumTriangles; + const std::string m_Header; + const IGeometry::MeshIndexArrayType& m_Triangles; + const Float32Array& m_Vertices; + const Int32Array& m_FeatureIds; + const int32 m_FeatureId; +}; } // namespace // ----------------------------------------------------------------------------- @@ -262,7 +424,7 @@ Result<> WriteStlFile::operator()() auto groupingType = static_cast(m_InputValues->GroupingType); - if(groupingType == GroupingType::None) + if(groupingType == GroupingType::SingleFile) { auto atomicFileResult = AtomicFile::Create(m_InputValues->OutputStlFile); if(atomicFileResult.invalid()) @@ -306,91 +468,133 @@ Result<> WriteStlFile::operator()() } } + // The writing of the files can happen in parallel as much as the Operating System will allow + ParallelTaskAlgorithm taskRunner; + taskRunner.setParallelizationEnabled(true); + // Store a list of Atomic Files, so we can clean up or finish depending on the outcome of all the writes std::vector> fileList; - { // Scope to cut overhead and ensure file lock is released on windows + if(groupingType == GroupingType::Features) + { 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()); - fileList.reserve(uniqueGrainIds.size()); + // Faster and more memory efficient since we don't need phases + std::unordered_set uniqueGrainIds(featureIds.cbegin(), featureIds.cend()); - usize fileIndex = 0; - for(const auto featureId : uniqueGrainIds) + fileList.reserve(uniqueGrainIds.size()); + + usize fileIndex = 0; + for(const auto featureId : uniqueGrainIds) + { + // Generate the output file + fileList.push_back(AtomicFile::Create(m_InputValues->OutputStlDirectory / fmt::format("{}Feature_{}.stl", m_InputValues->OutputStlPrefix, featureId))); + if(fileList[fileIndex].invalid()) { - // Generate the output file - fileList.push_back(AtomicFile::Create(m_InputValues->OutputStlDirectory / fmt::format("{}Feature_{}.stl", m_InputValues->OutputStlPrefix, featureId))); - if(fileList[fileIndex].invalid()) - { - return ConvertResult(std::move(fileList[fileIndex])); - } - - m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing STL for Feature Id {}", featureId)); - - auto result = ::MultiWriteOutStl(fileList[fileIndex].value().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++; + return ConvertResult(std::move(fileList[fileIndex])); } + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing STL for Feature Id {}", featureId)); + taskRunner.execute(MultiWriteStlFileImpl(this, fileList[fileIndex].value().tempFilePath(), nTriangles, {"DREAM3D Generated For Feature ID " + StringUtilities::number(featureId)}, triangles, + vertices, featureIds, featureId)); + fileIndex++; + if(m_HasErrors) + { + break; + } + } + taskRunner.wait(); + } + + if(groupingType == GroupingType::FeaturesAndPhases) + { + const auto& featureIds = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsPath); + + 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]); } - if(groupingType == GroupingType::FeaturesAndPhases) + + // Loop over the unique feature Ids + usize fileIndex = 0; + for(const auto& [featureId, value] : uniqueGrainIdToPhase) { - std::map uniqueGrainIdToPhase; + // Generate the output file + fileList.push_back(AtomicFile::Create(m_InputValues->OutputStlDirectory / fmt::format("{}Ensemble_{}_Feature_{}.stl", m_InputValues->OutputStlPrefix, value, featureId))); - const auto& featurePhases = m_DataStructure.getDataRefAs(m_InputValues->FeaturePhasesPath); - for(IGeometry::MeshIndexType i = 0; i < nTriangles; i++) + if(fileList[fileIndex].invalid()) + { + return ConvertResult(std::move(fileList[fileIndex])); + } + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing STL for Phase {} and Feature Id {}", value, featureId)); + taskRunner.execute(MultiWriteStlFileImpl(this, fileList[fileIndex].value().tempFilePath(), nTriangles, + {"DREAM3D Generated For Feature ID " + StringUtilities::number(featureId) + " Phase " + StringUtilities::number(value)}, triangles, vertices, featureIds, + featureId)); + fileIndex++; + if(m_HasErrors) { - uniqueGrainIdToPhase.emplace(featureIds[i * 2], featurePhases[i * 2]); - uniqueGrainIdToPhase.emplace(featureIds[i * 2 + 1], featurePhases[i * 2 + 1]); + break; } + } + taskRunner.wait(); + } - // Loop over the unique feature Ids - usize fileIndex = 0; - for(const auto& [featureId, value] : uniqueGrainIdToPhase) + // Group Triangles by Part Number which is a single component Int32 Array + if(groupingType == GroupingType::PartNumber) + { + const auto& partNumbers = m_DataStructure.getDataRefAs(m_InputValues->PartNumberPath); + // Faster and more memory efficient since we don't need phases + // Build up a list of the unique Part Numbers + std::unordered_set uniquePartNumbers(partNumbers.cbegin(), partNumbers.cend()); + fileList.reserve(uniquePartNumbers.size()); // Reserved enough file names + + // Loop over each Part Number and write a file + usize fileIndex = 0; + for(const auto currentPartNumber : uniquePartNumbers) + { + // Generate the output file + fileList.push_back(AtomicFile::Create(m_InputValues->OutputStlDirectory / fmt::format("{}{}.stl", m_InputValues->OutputStlPrefix, currentPartNumber))); + if(fileList[fileIndex].invalid()) { - // Generate the output file - fileList.push_back(AtomicFile::Create(m_InputValues->OutputStlDirectory / fmt::format("{}Ensemble_{}_Feature_{}.stl", m_InputValues->OutputStlPrefix, value, featureId))); - - if(fileList[fileIndex].invalid()) - { - return ConvertResult(std::move(fileList[fileIndex])); - } - - m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing STL for Feature Id {}", featureId)); - - auto result = - ::MultiWriteOutStl(fileList[fileIndex].value().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++; + return ConvertResult(std::move(fileList[fileIndex])); + } + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing STL for Part Number {}", currentPartNumber)); + taskRunner.execute(MultiWriteStlFileImpl(this, fileList[fileIndex].value().tempFilePath(), nTriangles, {"DREAM3D Generated For Part Number " + StringUtilities::number(currentPartNumber)}, + triangles, vertices, partNumbers, currentPartNumber)); + fileIndex++; + if(m_HasErrors) + { + break; } } + taskRunner.wait(); } + // Commit all the temp files for(auto& atomicFile : fileList) { Result<> commitResult = atomicFile.value().commit(); if(commitResult.invalid()) { - return commitResult; + m_Result = MergeResults(m_Result, commitResult); } } - return {}; + return m_Result; +} + +// ----------------------------------------------------------------------------- +void WriteStlFile::sendThreadSafeProgressMessage(Result<> result) +{ + std::lock_guard guard(m_ProgressMessage_Mutex); + if(result.invalid()) + { + m_HasErrors = true; + m_Result = MergeResults(m_Result, result); + } } // 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 72d464155a..b99aec26c2 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteStlFile.hpp @@ -17,7 +17,8 @@ enum class GroupingType : ChoicesParameter::ValueType { Features, FeaturesAndPhases, - None + SingleFile, + PartNumber }; struct SIMPLNXCORE_EXPORT WriteStlFileInputValues @@ -29,6 +30,7 @@ struct SIMPLNXCORE_EXPORT WriteStlFileInputValues DataPath FeatureIdsPath; DataPath FeaturePhasesPath; DataPath TriangleGeomPath; + DataPath PartNumberPath; }; /** @@ -49,11 +51,17 @@ class SIMPLNXCORE_EXPORT WriteStlFile const std::atomic_bool& getCancel(); + void sendThreadSafeProgressMessage(Result<> result); + private: DataStructure& m_DataStructure; const WriteStlFileInputValues* m_InputValues = nullptr; const std::atomic_bool& m_ShouldCancel; const IFilter::MessageHandler& m_MessageHandler; + mutable std::mutex m_ProgressMessage_Mutex; + + mutable bool m_HasErrors = false; + Result<> m_Result; }; } // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CombineStlFilesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CombineStlFilesFilter.cpp index 5f418e7e57..0c3de7c2e5 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CombineStlFilesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CombineStlFilesFilter.cpp @@ -5,7 +5,9 @@ #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Filter/Actions/CreateArrayAction.hpp" +#include "simplnx/Filter/Actions/CreateAttributeMatrixAction.hpp" #include "simplnx/Filter/Actions/CreateGeometry2DAction.hpp" +#include "simplnx/Filter/Actions/CreateStringArrayAction.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/DataGroupCreationParameter.hpp" @@ -61,12 +63,14 @@ Parameters CombineStlFilesFilter::parameters() const params.insert(std::make_unique(k_StlFilesPath_Key, "Path to STL Files", "The path to the folder containing all the STL files to be combined", fs::path(""), FileSystemPathParameter::ExtensionsType{}, FileSystemPathParameter::PathType::InputDir)); - params.insertLinkableParameter(std::make_unique(k_LabelFaces_Key, "Label Triangles", "When true, each triangle will get an index associated with the index of the STL file", true)); - params.insert(std::make_unique(k_FaceLabelName_Key, "Created Face Labels", "The name of the face labels data array", "FileIndex")); + params.insertLinkableParameter( + std::make_unique(k_LabelFaces_Key, "Generate Triangle Part Numbers", "When true, each triangle will get an index associated with the index of the STL file", true)); + params.insert(std::make_unique(k_FaceLabelName_Key, "Created Part Number Array", "The name of the part numbers data array", "Part Number")); params.linkParameters(k_LabelFaces_Key, k_FaceLabelName_Key, true); - params.insertLinkableParameter(std::make_unique(k_LabelVertices_Key, "Label Vertices", "When true, each vertex will get an index associated with the index of the STL file", true)); - params.insert(std::make_unique(k_VertexLabelName_Key, "Created Vertex Labels", "The name of the vertex labels data array", "FileIndex")); + params.insertLinkableParameter( + std::make_unique(k_LabelVertices_Key, "Generate Vertex Part Numbers", "When true, each vertex will get an index associated with the index of the STL file", true)); + params.insert(std::make_unique(k_VertexLabelName_Key, "Created Part Number Labels", "The name of the part numbers data array", "Part Number")); params.linkParameters(k_LabelVertices_Key, k_VertexLabelName_Key, true); params.insertSeparator(Parameters::Separator{"Output Geometry"}); @@ -81,6 +85,14 @@ Parameters CombineStlFilesFilter::parameters() const params.insert(std::make_unique(k_VertexAttributeMatrixName_Key, "Vertex Attribute Matrix", "The name of the vertex level attribute matrix to be created with the geometry", TriangleGeom::k_VertexDataName)); + params.insertSeparator(Parameters::Separator{"Output Feature Data"}); + params.insert(std::make_unique(k_CellFeatureAttributeMatrixName_Key, "Feature Attribute Matrix", "The name of the created feature attribute matrix", "Cell Feature Data")); + params.insert(std::make_unique( + k_ActiveArrayName_Key, "Active", + "Specifies if the Feature is still in the sample (true if the Feature is in the sample and false if it is not). At the end of the Filter, all Features will be Active", "Active")); + params.insert( + std::make_unique(k_FileListName_Key, "File List Array", "The path to a String array that will store the input paths of each file that was read.", "STL File List")); + return params; } @@ -128,6 +140,7 @@ IFilter::PreflightResult CombineStlFilesFilter::preflightImpl(const DataStructur CreateTriangleGeometryAction::k_DefaultVerticesName, CreateTriangleGeometryAction::k_DefaultFacesName); resultOutputActions.value().appendAction(std::move(createTriangleGeometryAction)); } + DataPath faceAttributeMatrixDataPath = pTriangleDataContainerNameValue.createChildPath(pFaceAttributeMatrixNameValue); // Create the Triangle Normals path { @@ -140,7 +153,7 @@ IFilter::PreflightResult CombineStlFilesFilter::preflightImpl(const DataStructur if(createFaceLabels) { auto facePath = faceAttributeMatrixDataPath.createChildPath(faceLabelsName); - auto createArrayAction = std::make_unique(nx::core::DataType::uint32, std::vector{1}, std::vector{1}, facePath); + auto createArrayAction = std::make_unique(nx::core::DataType::int32, std::vector{1}, std::vector{1}, facePath); resultOutputActions.value().appendAction(std::move(createArrayAction)); } @@ -148,10 +161,29 @@ IFilter::PreflightResult CombineStlFilesFilter::preflightImpl(const DataStructur if(createVertexLabels) { auto vertexPath = pTriangleDataContainerNameValue.createChildPath(pVertexAttributeMatrixNameValue).createChildPath(vertexLabelsName); - auto createArrayAction = std::make_unique(nx::core::DataType::uint32, std::vector{1}, std::vector{1}, vertexPath); + auto createArrayAction = std::make_unique(nx::core::DataType::int32, std::vector{1}, std::vector{1}, vertexPath); resultOutputActions.value().appendAction(std::move(createArrayAction)); } + { + auto pCellFeatureAttributeMatrixName = filterArgs.value(k_CellFeatureAttributeMatrixName_Key); + auto pActiveArrayName = filterArgs.value(k_ActiveArrayName_Key); + auto fileListArrayName = filterArgs.value(k_FileListName_Key); + + auto pCellFeatureAttributeMatrixPath = pTriangleDataContainerNameValue.createChildPath(pCellFeatureAttributeMatrixName); + auto activeArrayPath = pCellFeatureAttributeMatrixPath.createChildPath(pActiveArrayName); + auto fileListPath = pCellFeatureAttributeMatrixPath.createChildPath(fileListArrayName); + + // Create output feature data structure items + auto createFeatureGroupAction = std::make_unique(pCellFeatureAttributeMatrixPath, std::vector{stlFiles.size() + 1}); + auto createActiveAction = std::make_unique(DataType::uint8, std::vector{stlFiles.size() + 1}, std::vector{1}, activeArrayPath); + auto createFileListAction = std::make_unique(std::vector{stlFiles.size() + 1}, fileListPath); + + resultOutputActions.value().appendAction(std::move(createFeatureGroupAction)); + resultOutputActions.value().appendAction(std::move(createActiveAction)); + resultOutputActions.value().appendAction(std::move(createFileListAction)); + } + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } @@ -174,6 +206,10 @@ Result<> CombineStlFilesFilter::executeImpl(DataStructure& dataStructure, const inputValues.LabelVertices = filterArgs.value(k_LabelVertices_Key); inputValues.VertexFileIndexArrayPath = DataPath({inputValues.TriangleDataContainerName.getTargetName(), pVertexAttributeMatrixNameValue, filterArgs.value(k_VertexLabelName_Key)}); + inputValues.CellFeatureAttributeMatrixName = filterArgs.value(k_CellFeatureAttributeMatrixName_Key); + inputValues.ActiveArrayName = filterArgs.value(k_ActiveArrayName_Key); + inputValues.FileListArrayName = filterArgs.value(k_FileListName_Key); + return CombineStlFiles(dataStructure, messageHandler, shouldCancel, &inputValues)(); } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CombineStlFilesFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CombineStlFilesFilter.hpp index 495a9a1aa1..f75965f07b 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CombineStlFilesFilter.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CombineStlFilesFilter.hpp @@ -33,6 +33,9 @@ class SIMPLNXCORE_EXPORT CombineStlFilesFilter : public IFilter static inline constexpr StringLiteral k_FaceLabelName_Key = "face_label_name"; static inline constexpr StringLiteral k_LabelVertices_Key = "label_vertices"; static inline constexpr StringLiteral k_VertexLabelName_Key = "vertex_label_name"; + static inline constexpr StringLiteral k_CellFeatureAttributeMatrixName_Key = "cell_feature_attribute_matrix_name"; + static inline constexpr StringLiteral k_ActiveArrayName_Key = "active_array_name"; + static inline constexpr StringLiteral k_FileListName_Key = "output_file_list_name"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.cpp index 7d6a88f036..f4ad6934ef 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.cpp @@ -58,11 +58,11 @@ Parameters WriteStlFileFilter::parameters() const // Create the parameter descriptors that are needed for this filter params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); 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 + ChoicesParameter::Choices{"Features", "Phases and Features", "Single File", "Part Index"})); // 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_OutputStlPrefix_Key, "Output 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)); @@ -74,7 +74,8 @@ Parameters WriteStlFileFilter::parameters() const ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{2}})); 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}})); - + params.insert(std::make_unique(k_PartNumberPath_Key, "Part Numbers", "The Part Numbers to order/index files by", DataPath{}, + ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); // 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)); @@ -88,7 +89,12 @@ Parameters WriteStlFileFilter::parameters() const 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)); + params.linkParameters(k_GroupingType_Key, k_OutputStlFile_Key, to_underlying(GroupingType::SingleFile)); + + //--------------- Part Number ---------------- + params.linkParameters(k_GroupingType_Key, k_OutputStlDirectory_Key, to_underlying(GroupingType::PartNumber)); + params.linkParameters(k_GroupingType_Key, k_OutputStlPrefix_Key, to_underlying(GroupingType::PartNumber)); + params.linkParameters(k_GroupingType_Key, k_PartNumberPath_Key, to_underlying(GroupingType::PartNumber)); return params; } @@ -108,6 +114,7 @@ IFilter::PreflightResult WriteStlFileFilter::preflightImpl(const DataStructure& auto pTriangleGeomPathValue = filterArgs.value(k_TriangleGeomPath_Key); auto pFeatureIdsPathValue = filterArgs.value(k_FeatureIdsPath_Key); auto pFeaturePhasesPathValue = filterArgs.value(k_FeaturePhasesPath_Key); + auto pPartNumberPathValue = filterArgs.value(k_PartNumberPath_Key); PreflightResult preflightResult; nx::core::Result resultOutputActions; @@ -125,6 +132,14 @@ 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(pGroupingTypeValue == GroupingType::Features || pGroupingTypeValue == GroupingType::FeaturesAndPhases) + { + if(auto* featureIds = dataStructure.getDataAs(pFeatureIdsPathValue); featureIds == nullptr) + { + return MakePreflightErrorResult(-27873, fmt::format("Feature Ids Array doesn't exist at: {}", pFeatureIdsPathValue.toString())); + } + } + if(pGroupingTypeValue == GroupingType::FeaturesAndPhases) { if(auto* featurePhases = dataStructure.getDataAs(pFeaturePhasesPathValue); featurePhases == nullptr) @@ -132,14 +147,14 @@ IFilter::PreflightResult WriteStlFileFilter::preflightImpl(const DataStructure& return MakePreflightErrorResult(-27872, fmt::format("Feature Phases Array doesn't exist at: {}", pFeaturePhasesPathValue.toString())); } } - if(pGroupingTypeValue != GroupingType::None) + + if(pGroupingTypeValue == GroupingType::PartNumber) { - if(auto* featureIds = dataStructure.getDataAs(pFeatureIdsPathValue); featureIds == nullptr) + if(auto* featureIds = dataStructure.getDataAs(pPartNumberPathValue); featureIds == nullptr) { - return MakePreflightErrorResult(-27873, fmt::format("Feature Ids Array doesn't exist at: {}", pFeatureIdsPathValue.toString())); + return MakePreflightErrorResult(-27874, fmt::format("Part Number Array doesn't exist at: {}", pPartNumberPathValue.toString())); } } - // Return both the resultOutputActions and the preflightUpdatedValues via std::move() return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } @@ -157,7 +172,7 @@ Result<> WriteStlFileFilter::executeImpl(DataStructure& dataStructure, const Arg inputValues.FeatureIdsPath = filterArgs.value(k_FeatureIdsPath_Key); inputValues.FeaturePhasesPath = filterArgs.value(k_FeaturePhasesPath_Key); inputValues.TriangleGeomPath = filterArgs.value(k_TriangleGeomPath_Key); - + inputValues.PartNumberPath = filterArgs.value(k_PartNumberPath_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 847301ff88..d2c9290fe4 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteStlFileFilter.hpp @@ -30,6 +30,7 @@ class SIMPLNXCORE_EXPORT WriteStlFileFilter : public IFilter 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_PartNumberPath_Key = "part_number_path"; static inline constexpr StringLiteral k_TriangleGeomPath_Key = "input_triangle_geometry_path"; /** diff --git a/src/Plugins/SimplnxCore/test/WriteStlFileTest.cpp b/src/Plugins/SimplnxCore/test/WriteStlFileTest.cpp index 6a68c31990..80f81e6895 100644 --- a/src/Plugins/SimplnxCore/test/WriteStlFileTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteStlFileTest.cpp @@ -1,5 +1,6 @@ #include +#include "SimplnxCore/Filters/CombineStlFilesFilter.hpp" #include "SimplnxCore/Filters/WriteStlFileFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" @@ -10,13 +11,16 @@ #include "simplnx/UnitTest/UnitTestCommon.hpp" #include - namespace fs = std::filesystem; -using namespace nx::core; +using namespace nx::core; +using namespace nx::core::Constants; namespace { const std::string k_ExemplarDir = fmt::format("{}/6_6_write_stl_file_test", unit_test::k_TestFilesDir); +const DataPath k_ComputedTriangleDataContainerName({"ComputedTriangleDataContainer"}); +const DataPath k_ExemplarTriangleDataContainerName({k_TriangleDataContainerName}); +const std::string k_PartNumberName = "Part Number"; std::vector readIn(fs::path filePath) { @@ -116,3 +120,72 @@ TEST_CASE("SimplnxCore::WriteStlFileFilter: Single File Valid", "[SimplnxCore][W ::CompareSingleResult(); } + +TEST_CASE("SimplnxCore::WriteStlFileFilter:Part_Number", "[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"); + + const nx::core::UnitTest::TestFileSentinel testDataSentinel2(nx::core::unit_test::k_CMakeExecutable, nx::core::unit_test::k_TestFilesDir, "6_6_combine_stl_files_v2.tar.gz", + "6_6_combine_stl_files.dream3d"); + DataStructure dataStructure; + + { + CombineStlFilesFilter filter; + Arguments args; + std::string inputStlDir = fmt::format("{}/6_6_combine_stl_files_v2/STL_Models", unit_test::k_TestFilesDir.view()); + + // Create default Parameters for the filter. + args.insertOrAssign(CombineStlFilesFilter::k_StlFilesPath_Key, std::make_any(fs::path(inputStlDir))); + args.insertOrAssign(CombineStlFilesFilter::k_TriangleGeometryPath_Key, std::make_any(k_ComputedTriangleDataContainerName)); + args.insertOrAssign(CombineStlFilesFilter::k_FaceAttributeMatrixName_Key, std::make_any(k_FaceData)); + args.insertOrAssign(CombineStlFilesFilter::k_FaceNormalsArrayName_Key, std::make_any("Face Normals")); + args.insertOrAssign(CombineStlFilesFilter::k_VertexAttributeMatrixName_Key, std::make_any(k_VertexData)); + args.insertOrAssign(CombineStlFilesFilter::k_LabelFaces_Key, std::make_any(true)); + args.insertOrAssign(CombineStlFilesFilter::k_FaceLabelName_Key, std::make_any(k_PartNumberName)); + args.insertOrAssign(CombineStlFilesFilter::k_LabelVertices_Key, std::make_any(true)); + args.insertOrAssign(CombineStlFilesFilter::k_VertexLabelName_Key, std::make_any(k_PartNumberName)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } + + { + // 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(3)); + 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("Part_Number_")); + args.insertOrAssign(WriteStlFileFilter::k_TriangleGeomPath_Key, std::make_any(k_ComputedTriangleDataContainerName)); + args.insertOrAssign(WriteStlFileFilter::k_PartNumberPath_Key, std::make_any(k_ComputedTriangleDataContainerName.createChildPath(k_FaceData).createChildPath(k_PartNumberName))); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } + + fs::path writtenFilePath = fs::path(std::string(unit_test::k_BinaryTestOutputDir) + "/Part_Number_1.stl"); + REQUIRE(fs::exists(writtenFilePath)); + auto fileContents = readIn(writtenFilePath); + std::string md5Hash = nx::core::UnitTest::ComputeMD5Hash(fileContents); + REQUIRE(md5Hash == "a0383b898d0668d70f08e135e5064efb"); + + writtenFilePath = fs::path(std::string(unit_test::k_BinaryTestOutputDir) + "/Part_Number_2.stl"); + REQUIRE(fs::exists(writtenFilePath)); + fileContents = readIn(writtenFilePath); + md5Hash = nx::core::UnitTest::ComputeMD5Hash(fileContents); + REQUIRE(md5Hash == "d45a0d99495df506384fdbbb46a79f5c"); +} diff --git a/src/simplnx/Common/AtomicFile.cpp b/src/simplnx/Common/AtomicFile.cpp index d45172e383..4cf13157b8 100644 --- a/src/simplnx/Common/AtomicFile.cpp +++ b/src/simplnx/Common/AtomicFile.cpp @@ -41,7 +41,7 @@ Result AtomicFile::Create(fs::path filename) atomicFile.m_FilePath = fs::absolute(atomicFile.m_FilePath); } catch(const std::filesystem::filesystem_error& error) { - return MakeErrorResult(-15780, fmt::format("When attempting to create an absolute path, AtomicFile encountered the following error: '{}'", error.what())); + return MakeErrorResult(-15780, fmt::format("AtomicFile Error: When attempting to create an absolute path, AtomicFile encountered the following error: '{}'", error.what())); } } @@ -95,7 +95,7 @@ Result<> AtomicFile::commit() { if(!fs::exists(m_TempFilePath)) { - return MakeErrorResult(-15780, m_TempFilePath.string() + " does not exist"); + return MakeErrorResult(-15781, fmt::format("AtomicFile Commit Error: {} does not exist", m_TempFilePath.string())); } try @@ -103,7 +103,8 @@ Result<> AtomicFile::commit() fs::rename(m_TempFilePath, m_FilePath); } catch(const std::filesystem::filesystem_error& error) { - return MakeErrorResult(-15780, fmt::format("When attempting to move the temp file to the end absolute path, AtomicFile encountered the following error on rename(): '{}'", error.what())); + return MakeErrorResult( + -15782, fmt::format("AtomicFile Commit Error: When attempting to move the temp file to the end absolute path, AtomicFile encountered the following error on rename(): '{}'", error.what())); } return {}; diff --git a/src/Plugins/ITKImageProcessing/test/MD5.cpp b/src/simplnx/Utilities/MD5.cpp similarity index 99% rename from src/Plugins/ITKImageProcessing/test/MD5.cpp rename to src/simplnx/Utilities/MD5.cpp index 41c82bcd6a..3a1a7d969f 100644 --- a/src/Plugins/ITKImageProcessing/test/MD5.cpp +++ b/src/simplnx/Utilities/MD5.cpp @@ -59,6 +59,8 @@ documentation and/or software. /////////////////////////////////////////////// +using namespace nx::core; + // F, G, H and I are basic MD5 functions. MD5::uint4 MD5::F(uint4 x, uint4 y, uint4 z) { diff --git a/src/Plugins/ITKImageProcessing/test/MD5.hpp b/src/simplnx/Utilities/MD5.hpp similarity index 96% rename from src/Plugins/ITKImageProcessing/test/MD5.hpp rename to src/simplnx/Utilities/MD5.hpp index 4c474267cf..088fda05f1 100644 --- a/src/Plugins/ITKImageProcessing/test/MD5.hpp +++ b/src/simplnx/Utilities/MD5.hpp @@ -30,6 +30,7 @@ These notices must be retained in any copies of any part of this documentation and/or software. */ +#include "simplnx/simplnx_export.hpp" #include #include @@ -44,7 +45,10 @@ documentation and/or software. // MD5(std::string).hexdigest() // // assumes that char is 8 bit and int is 32 bit -class MD5 +namespace nx::core +{ + +class SIMPLNX_EXPORT MD5 { public: typedef unsigned int size_type; // must be 32bit @@ -89,3 +93,5 @@ class MD5 }; std::string md5(const std::string str); + +} // namespace nx::core diff --git a/test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp b/test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp index 7de7ebcc22..27b03cecd1 100644 --- a/test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp +++ b/test/UnitTestCommon/include/simplnx/UnitTest/UnitTestCommon.hpp @@ -19,6 +19,7 @@ #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" +#include "simplnx/Utilities/MD5.hpp" #include "simplnx/Utilities/Parsing/DREAM3D/Dream3dIO.hpp" #include "simplnx/Utilities/Parsing/HDF5/Writers/FileWriter.hpp" @@ -210,6 +211,17 @@ namespace UnitTest { inline constexpr float EPSILON = 0.0001; +template +std::string ComputeMD5Hash(const std::vector& outputDataArray) +{ + const T* dataPtr = outputDataArray.data(); + usize arraySize = outputDataArray.size(); + MD5 md5; + md5.update(reinterpret_cast(dataPtr), arraySize * sizeof(T)); + md5.finalize(); + return md5.hexdigest(); +} + /** * @brief This class will decompress a tar.gz file using the locally installed copy of cmake and when * then class goes out of scope the extracted contents will be deleted from disk.