diff --git a/src/Plugins/SimplnxCore/CMakeLists.txt b/src/Plugins/SimplnxCore/CMakeLists.txt index 65cfeb1df4..0ca373406c 100644 --- a/src/Plugins/SimplnxCore/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/CMakeLists.txt @@ -58,6 +58,7 @@ set(FilterList CreateGeometryFilter CreateImageGeometryFilter CreatePythonSkeletonFilter + CropEdgeGeometryFilter CropImageGeometryFilter CropVertexGeometryFilter DBSCANFilter @@ -174,6 +175,7 @@ set(AlgorithmList ConcatenateDataArrays ConvertColorToGrayScale ConvertData + CropEdgeGeometry CreatePythonSkeleton DBSCAN ErodeDilateBadData diff --git a/src/Plugins/SimplnxCore/docs/CropEdgeGeometryFilter.md b/src/Plugins/SimplnxCore/docs/CropEdgeGeometryFilter.md new file mode 100644 index 0000000000..eb38dd84eb --- /dev/null +++ b/src/Plugins/SimplnxCore/docs/CropEdgeGeometryFilter.md @@ -0,0 +1,100 @@ +# Crop Geometry (Edge) + +## Description + +The **Crop Geometry (Edge) Filter** allows users to crop a region of interest (ROI) from an **Edge Geometry**. This filter is essential for isolating specific portions of edge-based data structures. + +Users can selectively crop specific dimensions of the **Edge Geometry** by toggling **Crop X Dimension**, **Crop Y Dimension**, and **Crop Z Dimension** ON or OFF. Only dimensions that are turned ON will be cropped. + +### Boundary Intersection Behavior + +The filter provides options to handle edges that intersect the defined cropping bounds: + +- **Filter Error**: Throws an error if any edge intersects the cropping boundary, ensuring strict adherence to the ROI. +- **Interpolate Outside Vertex**: Interpolates the position of vertices that lie outside the ROI, allowing partial edges to be included based on interpolation. +- **Ignore Edge**: Excludes edges that intersect the cropping boundary without throwing an error or interpolating vertices. + +**NOTE:** When the **Interpolate Outside Vertex** boundary intersection behavior is chosen, this filter DOES NOT interpolate any vertex data, only the vertex position! + +## Examples +In the following examples, an edge geometry with bounds (-1, 1), (-1, 1), (0, 1) is being used: + +![Base edge geometry for examples](Images/Crop_Edge_Geometry/CropEdgeGeom1.png) + +### Filter Error Example + +| **Parameter** | **Value** | +|------------------------------------|-----------------------| +| **Crop X Dimension** | ON | +| **Crop Y Dimension** | OFF | +| **Crop Z Dimension** | OFF | +| **Min Coordinate** | (-1, 0, 0) | +| **Max Coordinate** | (0.2, 0, 0) | +| **Boundary Intersection Behavior** | Filter Error | + +Since **Crop X Dimension** is the only one turned on, only the X dimension will be cropped. This means that the filter will only use the X min/max coordinate values to crop the edge geometry; the Y & Z min/max coordinate values will be ignored. + +These inputs result in a filter execution error because an edge exists in the edge geometry with the following two vertices: + +(0.071339, -0.997452, 0.0) <--- Inside the X bounds of (-1, 0.2) +(0.212565, -0.977147, 0.0) <--- Outside the X bounds of (-1, 0.2) + +### Ignore Edges Example + +| **Parameter** | **Value** | +|------------------------------------|-----------------------| +| **Crop X Dimension** | ON | +| **Crop Y Dimension** | ON | +| **Crop Z Dimension** | OFF | +| **Min Coordinate** | (-0.7, -0.7, 0) | +| **Max Coordinate** | (0.7, 0.7, 0) | +| **Boundary Intersection Behavior** | Ignore Edge | + +Since **Crop X Dimension** and **Crop Y Dimension** are turned on, only the X & Y dimensions will be cropped. This means that the filter will only use the X & Y min/max coordinate values to crop the edge geometry; the Z min/max coordinate values will be ignored. + +These inputs result in the following edge geometry output: + +![Ignore Edges Example](Images/Crop_Edge_Geometry/CropEdgeGeom2.png) + +The original edge geometry is orange, the cropped edge geometry (which overlaps the orange) is in green. + +As you can see, any edges that contain vertices that are outside (-0.7, 0.7) in the X dimension and (-0.7, 0.7) in the Y dimension have been cropped out. + +### Interpolate Outside Vertices Example + +| **Parameter** | **Value** | +|------------------------------------|----------------------------| +| **Crop X Dimension** | ON | +| **Crop Y Dimension** | ON | +| **Crop Z Dimension** | OFF | +| **Min Coordinate** | (-0.7, -0.7, 0) | +| **Max Coordinate** | (0.7, 0.7, 0) | +| **Boundary Intersection Behavior** | Interpolate Outside Vertex | + +Let's take the same example as the previous, but instead set the **Boundary Intersection Behavior** to interpolate outside vertices. + +Again, since **Crop X Dimension** and **Crop Y Dimension** are turned on, only the X & Y dimensions will be cropped. This means that the filter will only use the X & Y min/max coordinate values to crop the edge geometry; the Z min/max coordinate values will be ignored. + +Unlike the last example, however, this time the filter will interpolate any outside vertices of edges that are intersecting the ROI boundary so that those edges can still be included in the final edge geometry: + +![Interpolate Outside Vertex Example](Images/Crop_Edge_Geometry/CropEdgeGeom3.png) + +The original edge geometry is orange, the cropped edge geometry (which overlaps the orange) is in green. + +![Interpolate Outside Vertex Example](Images/Crop_Edge_Geometry/CropEdgeGeom4.png) + +Three outside vertices, that have been interpolated to lie within the boundary, are circled in blue. + +As you can see, the edges that intersect the ROI boundary have been interpolated so that the outside vertex lies exactly on the boundary edge. Edges that have both vertices outside the boundary are cropped out completely. + +% 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/docs/Images/Crop_Edge_Geometry/CropEdgeGeom1.png b/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom1.png new file mode 100644 index 0000000000..3bdc346da8 Binary files /dev/null and b/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom1.png differ diff --git a/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom2.png b/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom2.png new file mode 100644 index 0000000000..703b13cd97 Binary files /dev/null and b/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom2.png differ diff --git a/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom3.png b/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom3.png new file mode 100644 index 0000000000..a4a637811a Binary files /dev/null and b/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom3.png differ diff --git a/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom4.png b/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom4.png new file mode 100644 index 0000000000..972c96bd9e Binary files /dev/null and b/src/Plugins/SimplnxCore/docs/Images/Crop_Edge_Geometry/CropEdgeGeom4.png differ diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CropEdgeGeometry.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CropEdgeGeometry.cpp new file mode 100644 index 0000000000..3c6e851a5e --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CropEdgeGeometry.cpp @@ -0,0 +1,405 @@ +#include "CropEdgeGeometry.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataGroup.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" +#include "simplnx/Utilities/ParallelDataAlgorithm.hpp" + +using namespace nx::core; + +namespace +{ +/** + * @brief + * @tparam T + */ +template +class CropEdgeGeomArray +{ +public: + CropEdgeGeomArray(const IDataArray& oldCellArray, IDataArray& newCellArray, const AttributeMatrix& srcAttrMatrix, const std::vector& tupleMask, const std::atomic_bool& shouldCancel) + : m_OldCellStore(oldCellArray.template getIDataStoreRefAs>()) + , m_NewCellStore(newCellArray.template getIDataStoreRefAs>()) + , m_SrcAttrMatrix(srcAttrMatrix) + , m_TupleMask(tupleMask) + , m_ShouldCancel(shouldCancel) + { + } + + ~CropEdgeGeomArray() = default; + + CropEdgeGeomArray(const CropEdgeGeomArray&) = default; + CropEdgeGeomArray(CropEdgeGeomArray&&) noexcept = default; + CropEdgeGeomArray& operator=(const CropEdgeGeomArray&) = delete; + CropEdgeGeomArray& operator=(CropEdgeGeomArray&&) noexcept = delete; + + void operator()() const + { + usize newIndex = 0; + for(usize i = 0; i < m_SrcAttrMatrix.getNumTuples(); ++i) + { + if(m_ShouldCancel) + { + return; + } + else if(m_TupleMask[i]) + { + CopyFromArray::CopyData(m_OldCellStore, m_NewCellStore, newIndex, i, 1); + newIndex++; + } + } + } + +private: + const AbstractDataStore& m_OldCellStore; + AbstractDataStore& m_NewCellStore; + const AttributeMatrix& m_SrcAttrMatrix; + const std::vector m_TupleMask; + const std::atomic_bool& m_ShouldCancel; +}; + +/** + * @brief Clips the parameter t_min and t_max based on the provided coordinates and bounds. + * + * This function adjusts the t_min and t_max parameters to ensure that the interpolated + * point lies within the specified minimum and maximum bounds for a single axis. + * + * @param t_min Current minimum value of the interpolation parameter t. + * @param t_max Current maximum value of the interpolation parameter t. + * @param v1 Coordinate of the inside vertex along a specific axis. + * @param v2 Coordinate of the outside vertex along the same axis. + * @param vmin Minimum allowable value along the axis. + * @param vmax Maximum allowable value along the axis. + * @return std::pair Updated (t_min, t_max) after clipping. + */ +std::pair clip(float32 t_min, float32 t_max, float32 v1, float32 v2, float32 vmin, float32 vmax) +{ + if(v1 < v2) + { // Moving towards increasing coordinate + if(v2 > vmax) + { + t_max = std::min(t_max, (vmax - v1) / (v2 - v1)); + } + if(v2 < vmin) + { + t_min = std::max(t_min, (vmin - v1) / (v2 - v1)); + } + } + else + { // Moving towards decreasing coordinate + if(v2 < vmin) + { + t_max = std::min(t_max, (vmin - v1) / (v2 - v1)); + } + if(v2 > vmax) + { + t_min = std::max(t_min, (vmax - v1) / (v2 - v1)); + } + } + return {t_min, t_max}; +} + +/** + * @brief Interpolates the position of an outside vertex to lie within the bounding box. + * + * This function adjusts the coordinates of an outside vertex so that it lies on the boundary + * of the specified bounding box along the line connecting it to an inside vertex. + * + * @param insideVertex 3D coordinates of the inside vertex as a tuple (x, y, z). + * @param outsideVertex 3D coordinates of the outside vertex as a tuple (x, y, z). + * @param boundingBox The bounding box to interpolate the outside vertex to lie within. + * MUST be in the format (x_min, y_min, z_min, x_max, y_max, z_max). + * @return std::tuple Adjusted 3D coordinates of the outside vertex. + */ +std::tuple interpolate_outside_vertex(const std::tuple& insideVertex, const std::tuple& outsideVertex, + std::array boundingBox) +{ + float32 x1 = std::get<0>(insideVertex); + float32 y1 = std::get<1>(insideVertex); + float32 z1 = std::get<2>(insideVertex); + float32 x2 = std::get<0>(outsideVertex); + float32 y2 = std::get<1>(outsideVertex); + float32 z2 = std::get<2>(outsideVertex); + + float32 t_min = 0.0f; + float32 t_max = 1.0f; + + float32 x_min = boundingBox[0]; + float32 y_min = boundingBox[1]; + float32 z_min = boundingBox[2]; + float32 x_max = boundingBox[3]; + float32 y_max = boundingBox[4]; + float32 z_max = boundingBox[5]; + + // Apply clipping for each axis: X, Y, Z + std::tie(t_min, t_max) = clip(t_min, t_max, x1, x2, x_min, x_max); + std::tie(t_min, t_max) = clip(t_min, t_max, y1, y2, y_min, y_max); + std::tie(t_min, t_max) = clip(t_min, t_max, z1, z2, z_min, z_max); + + if(t_min > t_max) + { + // No intersection with the bounding box; return the original outside vertex + return {x2, y2, z2}; + } + + // Use t_max to find the intersection point on the bounding box + float32 t = t_max; + + // Calculate the new coordinates by interpolating with parameter t + float32 new_x = x1 + t * (x2 - x1); + float32 new_y = y1 + t * (y2 - y1); + float32 new_z = z1 + t * (z2 - z1); + + return {new_x, new_y, new_z}; +} + +bool is_inside(float32 x, float32 y, float32 z, const std::array& boundingBox) +{ + return (x >= boundingBox[0] && x <= boundingBox[3]) && (y >= boundingBox[1] && y <= boundingBox[4]) && (z >= boundingBox[2] && z <= boundingBox[5]); +} +} // namespace + +// ----------------------------------------------------------------------------- +CropEdgeGeometry::CropEdgeGeometry(DataStructure& dataStructure, const IFilter::MessageHandler& msgHandler, const std::atomic_bool& shouldCancel, CropEdgeGeometryInputValues* inputValues) +: m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(msgHandler) +{ +} + +// ----------------------------------------------------------------------------- +CropEdgeGeometry::~CropEdgeGeometry() noexcept = default; + +// ----------------------------------------------------------------------------- +const std::atomic_bool& CropEdgeGeometry::getCancel() +{ + return m_ShouldCancel; +} + +// ----------------------------------------------------------------------------- +Result<> CropEdgeGeometry::operator()() +{ + DataPath destEdgeGeomPath = m_InputValues->destEdgeGeomPath; + if(m_InputValues->removeOriginalGeometry) + { + auto tempPathVector = m_InputValues->srcEdgeGeomPath.getPathVector(); + auto tempName = k_TempGeometryName; + tempPathVector.back() = tempName; + destEdgeGeomPath = DataPath({tempPathVector}); + } + + float32 xMin = m_InputValues->cropXDim ? m_InputValues->minCoords[0] : std::numeric_limits::lowest(); + float32 xMax = m_InputValues->cropXDim ? m_InputValues->maxCoords[0] : std::numeric_limits::max(); + float32 yMin = m_InputValues->cropYDim ? m_InputValues->minCoords[1] : std::numeric_limits::lowest(); + float32 yMax = m_InputValues->cropYDim ? m_InputValues->maxCoords[1] : std::numeric_limits::max(); + float32 zMin = m_InputValues->cropZDim ? m_InputValues->minCoords[2] : std::numeric_limits::lowest(); + float32 zMax = m_InputValues->cropZDim ? m_InputValues->maxCoords[2] : std::numeric_limits::max(); + std::array boundingBox = {xMin, yMin, zMin, xMax, yMax, zMax}; + + auto& srcEdgeGeom = m_DataStructure.getDataRefAs(m_InputValues->srcEdgeGeomPath); + auto& srcVertices = srcEdgeGeom.getVerticesRef(); + auto& srcEdges = srcEdgeGeom.getEdgesRef(); + auto& srcVertexAttrMatrix = srcEdgeGeom.getVertexAttributeMatrixRef(); + auto& srcEdgesAttrMatrix = srcEdgeGeom.getEdgeAttributeMatrixRef(); + + auto& destEdgeGeom = m_DataStructure.getDataRefAs(destEdgeGeomPath); + auto& destVertexAttrMatrix = destEdgeGeom.getVertexAttributeMatrixRef(); + auto& destEdgesAttrMatrix = destEdgeGeom.getEdgeAttributeMatrixRef(); + auto& destVertices = destEdgeGeom.getVerticesRef(); + auto& destEdges = destEdgeGeom.getEdgesRef(); + + usize numVertices = srcVertices.getNumberOfTuples(); + usize numEdges = srcEdges.getNumberOfTuples(); + + auto behavior = static_cast(m_InputValues->boundaryIntersectionBehavior); + + // Resize vertices, vertex attribute matrix, edges, and edges attribute matrix to the maximum size + destVertices.resizeTuples({numVertices}); + destVertexAttrMatrix.resizeTuples({numVertices}); + destEdges.resizeTuples({numEdges}); + destEdgesAttrMatrix.resizeTuples({numEdges}); + + std::vector edgesMask(numEdges, false); + std::vector vertexReferenced(numVertices, false); + std::unordered_map> interpolatedValuesMap; + + for(usize i = 0; i < numEdges; ++i) + { + uint64 v0 = srcEdges[2 * i + 0]; + uint64 v1 = srcEdges[2 * i + 1]; + + // Validate vertex indices + if(v0 >= numVertices) + { + return MakeErrorResult(to_underlying(ErrorCodes::InvalidVertexIndex), fmt::format("Edge at index {} with value {} references an invalid vertex index.", 2 * i, v0)); + } + + if(v1 >= numVertices) + { + return MakeErrorResult(to_underlying(ErrorCodes::InvalidVertexIndex), fmt::format("Edge at index {} with value {} references an invalid vertex index.", 2 * i + 1, v1)); + } + + // Determine if each vertex is inside or outside + float32 x1 = srcVertices[3 * v0 + 0]; + float32 y1 = srcVertices[3 * v0 + 1]; + float32 z1 = srcVertices[3 * v0 + 2]; + bool v0_inside = is_inside(x1, y1, z1, boundingBox); + + float32 x2 = srcVertices[3 * v1 + 0]; + float32 y2 = srcVertices[3 * v1 + 1]; + float32 z2 = srcVertices[3 * v1 + 2]; + bool v1_inside = is_inside(x2, y2, z2, boundingBox); + + bool edgeOutsideBoundary = !v0_inside && !v1_inside; + bool edgeInsideBoundary = v0_inside && v1_inside; + bool edgeIntersectingBoundary = (v0_inside && !v1_inside) || (!v0_inside && v1_inside); + + if(edgeOutsideBoundary) + { + continue; + } + + if(edgeIntersectingBoundary) + { + // Found an edge intersecting the boundary + // Calculate the interpolated value for the outside vertex and store it in the interpolatedValuesMap + uint64 insideVertexIdx = v0; + uint64 outsideVertexIdx = v1; + if(v1_inside) + { + insideVertexIdx = v1; + outsideVertexIdx = v0; + } + + float32 insideX = srcVertices[3 * insideVertexIdx + 0]; + float32 insideY = srcVertices[3 * insideVertexIdx + 1]; + float32 insideZ = srcVertices[3 * insideVertexIdx + 2]; + float32 outsideX = srcVertices[3 * outsideVertexIdx + 0]; + float32 outsideY = srcVertices[3 * outsideVertexIdx + 1]; + float32 outsideZ = srcVertices[3 * outsideVertexIdx + 2]; + + if(behavior == BoundaryIntersectionBehavior::FilterError) + { + // Throw a filter error + std::string message = fmt::format("Edge {} connects inside vertex ({}, {}, {}) with outside vertex ({}, {}, {}). This intersects the bounds of ({}, {}, {}) and ({}, {}, {})", + std::to_string(i), std::to_string(insideX), std::to_string(insideY), std::to_string(insideZ), std::to_string(outsideX), std::to_string(outsideY), + std::to_string(outsideZ), boundingBox[0], boundingBox[1], boundingBox[2], boundingBox[3], boundingBox[4], boundingBox[5]); + return MakeErrorResult(to_underlying(ErrorCodes::OutsideVertexError), message); + } + else if(behavior == BoundaryIntersectionBehavior::InterpolateOutsideVertex) + { + // Calculate the interpolated value for the outside vertex and store it in the interpolatedValuesMap + interpolatedValuesMap[outsideVertexIdx] = interpolate_outside_vertex(std::make_tuple(insideX, insideY, insideZ), std::make_tuple(outsideX, outsideY, outsideZ), boundingBox); + } + } + + if(edgeInsideBoundary || (edgeIntersectingBoundary && behavior != BoundaryIntersectionBehavior::IgnoreEdge)) + { + vertexReferenced[v0] = true; + vertexReferenced[v1] = true; + edgesMask[i] = true; + } + } + + // Tally up the number of vertices referenced and edges kept + usize totalVerticesReferenced = std::count(vertexReferenced.begin(), vertexReferenced.end(), true); + usize totalEdgesKept = std::count(edgesMask.begin(), edgesMask.end(), true); + + // Resize to proper sizes + destVertices.resizeTuples({totalVerticesReferenced}); + destVertexAttrMatrix.resizeTuples({totalVerticesReferenced}); + destEdges.resizeTuples({totalEdgesKept}); + destEdgesAttrMatrix.resizeTuples({totalEdgesKept}); + + // Create a mapping from old vertex indices to new indices + std::vector vertexMapping(numVertices, -1); + + int64 newIndex = 0; + for(usize i = 0; i < numVertices; ++i) + { + if(vertexReferenced[i]) + { + vertexMapping[i] = newIndex; + if(behavior == BoundaryIntersectionBehavior::InterpolateOutsideVertex && interpolatedValuesMap.contains(i)) + { + destVertices[3 * newIndex + 0] = std::get<0>(interpolatedValuesMap[i]); + destVertices[3 * newIndex + 1] = std::get<1>(interpolatedValuesMap[i]); + destVertices[3 * newIndex + 2] = std::get<2>(interpolatedValuesMap[i]); + } + else + { + destVertices[3 * newIndex + 0] = srcVertices[3 * i + 0]; + destVertices[3 * newIndex + 1] = srcVertices[3 * i + 1]; + destVertices[3 * newIndex + 2] = srcVertices[3 * i + 2]; + } + newIndex++; + } + } + + // Crop each vertex data array in parallel + { + ParallelTaskAlgorithm taskRunner; + for(const auto& [dataId, oldDataObject] : srcVertexAttrMatrix) + { + if(m_ShouldCancel) + { + return {}; + } + + const auto& oldDataArray = dynamic_cast(*oldDataObject); + const std::string srcName = oldDataArray.getName(); + + auto& newDataArray = dynamic_cast(destVertexAttrMatrix.at(srcName)); + + m_MessageHandler(fmt::format("Cropping Volume || Copying Vertex Array {}", srcName)); + ExecuteParallelFunction(oldDataArray.getDataType(), taskRunner, oldDataArray, newDataArray, srcVertexAttrMatrix, vertexReferenced, m_ShouldCancel); + } + taskRunner.wait(); // This will spill over if the number of DataArrays to process does not divide evenly by the number of threads. + } + + // Create final edges with remapped vertex indices + newIndex = 0; + for(usize i = 0; i < numEdges; ++i) + { + if(edgesMask[i]) + { + // Get new vertices via indexing into vertex mapping with the old index from srcEdges + int64 new_v0 = vertexMapping[srcEdges[2 * i + 0]]; + int64 new_v1 = vertexMapping[srcEdges[2 * i + 1]]; + + // Validate mapping + if(new_v0 == -1 || new_v1 == -1) + { + return MakeErrorResult(to_underlying(ErrorCodes::InvalidVertexMapping), "Invalid vertex mapping during edge remapping."); + } + + destEdges[newIndex++] = static_cast(new_v0); + destEdges[newIndex++] = static_cast(new_v1); + } + } + + // Crop each edge data array in parallel + { + ParallelTaskAlgorithm taskRunner; + for(const auto& [dataId, oldDataObject] : srcEdgesAttrMatrix) + { + if(m_ShouldCancel) + { + return {}; + } + + const auto& oldDataArray = dynamic_cast(*oldDataObject); + const std::string srcName = oldDataArray.getName(); + + auto& newDataArray = dynamic_cast(destEdgesAttrMatrix.at(srcName)); + + m_MessageHandler(fmt::format("Cropping Volume || Copying Edge Array {}", srcName)); + ExecuteParallelFunction(oldDataArray.getDataType(), taskRunner, oldDataArray, newDataArray, srcEdgesAttrMatrix, edgesMask, m_ShouldCancel); + } + taskRunner.wait(); + } + + return {}; +} diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CropEdgeGeometry.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CropEdgeGeometry.hpp new file mode 100644 index 0000000000..864a989d18 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CropEdgeGeometry.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/Filter/IFilter.hpp" + +namespace +{ +const std::string k_TempGeometryName = ".cropped_edge_geometry"; +} + +namespace nx::core +{ + +struct SIMPLNXCORE_EXPORT CropEdgeGeometryInputValues +{ + DataPath srcEdgeGeomPath; + DataPath destEdgeGeomPath; + std::vector minCoords; + std::vector maxCoords; + bool removeOriginalGeometry; + bool cropXDim; + bool cropYDim; + bool cropZDim; + uint64 boundaryIntersectionBehavior; +}; + +/** + * @class + */ +class SIMPLNXCORE_EXPORT CropEdgeGeometry +{ +public: + CropEdgeGeometry(DataStructure& dataStructure, const IFilter::MessageHandler& msgHandler, const std::atomic_bool& shouldCancel, CropEdgeGeometryInputValues* inputValues); + ~CropEdgeGeometry() noexcept; + + CropEdgeGeometry(const CropEdgeGeometry&) = delete; + CropEdgeGeometry(CropEdgeGeometry&&) noexcept = delete; + CropEdgeGeometry& operator=(const CropEdgeGeometry&) = delete; + CropEdgeGeometry& operator=(CropEdgeGeometry&&) noexcept = delete; + + enum class BoundaryIntersectionBehavior : uint64 + { + InterpolateOutsideVertex = 0, + IgnoreEdge = 1, + FilterError = 2 + }; + + enum class ErrorCodes : int64 + { + XMinLargerThanXMax = -1210, + YMinLargerThanYMax = -1211, + ZMinLargerThanZMax = -1212, + NoDimensionsChosen = -1213, + InvalidVertexIndex = -1220, + OutsideVertexError = -1221, + InvalidVertexMapping = -1222 + }; + + Result<> operator()(); + + const std::atomic_bool& getCancel(); + +private: + DataStructure& m_DataStructure; + const CropEdgeGeometryInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; +}; + +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CropEdgeGeometryFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CropEdgeGeometryFilter.cpp new file mode 100644 index 0000000000..a7986c21d4 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CropEdgeGeometryFilter.cpp @@ -0,0 +1,303 @@ +#include "CropEdgeGeometryFilter.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/DataStructure/INeighborList.hpp" +#include "simplnx/Filter/Actions/CopyDataObjectAction.hpp" +#include "simplnx/Filter/Actions/CreateArrayAction.hpp" +#include "simplnx/Filter/Actions/CreateAttributeMatrixAction.hpp" +#include "simplnx/Filter/Actions/CreateGeometry1DAction.hpp" +#include "simplnx/Filter/Actions/DeleteDataAction.hpp" +#include "simplnx/Filter/Actions/RenameDataAction.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/DataGroupCreationParameter.hpp" +#include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Utilities/DataGroupUtilities.hpp" +#include "simplnx/Utilities/ParallelAlgorithmUtilities.hpp" +#include "simplnx/Utilities/ParallelDataAlgorithm.hpp" +#include "simplnx/Utilities/SIMPLConversion.hpp" +#include "simplnx/Utilities/StringUtilities.hpp" + +#include "SimplnxCore/Filters/Algorithms/CropEdgeGeometry.hpp" + +using namespace nx::core; + +//------------------------------------------------------------------------------ +CropEdgeGeometryFilter::CropEdgeGeometryFilter() +{ +} + +//------------------------------------------------------------------------------ +CropEdgeGeometryFilter::~CropEdgeGeometryFilter() noexcept +{ +} + +//------------------------------------------------------------------------------ +std::string CropEdgeGeometryFilter::name() const +{ + return FilterTraits::name; +} + +//------------------------------------------------------------------------------ +std::string CropEdgeGeometryFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid CropEdgeGeometryFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string CropEdgeGeometryFilter::humanName() const +{ + return "Crop Geometry (Edge)"; +} + +//------------------------------------------------------------------------------ +std::vector CropEdgeGeometryFilter::defaultTags() const +{ + return {className(), "Core", "Crop Edge Geometry", "Edge Geometry", "Conversion"}; +} + +//------------------------------------------------------------------------------ +Parameters CropEdgeGeometryFilter::parameters() const +{ + Parameters params; + + params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); + params.insert(std::make_unique(k_CropXDim_Key, "Crop X Dimension", "Enable cropping in the X dimension.", false)); + params.insert(std::make_unique(k_CropYDim_Key, "Crop Y Dimension", "Enable cropping in the Y dimension.", false)); + params.insert(std::make_unique(k_CropZDim_Key, "Crop Z Dimension", "Enable cropping in the Z dimension.", false)); + params.insert(std::make_unique(k_MinCoord_Key, "Min Coordinate", "Lower bound of the edge geometry to crop.", std::vector{0.0, 0.0, 0.0}, + std::vector{"X", "Y", "Z"})); + params.insert(std::make_unique(k_MaxCoord_Key, "Max Coordinate [Inclusive]", "Upper bound of the edge geometry to crop.", std::vector{0.0, 0.0, 0.0}, + std::vector{"X", "Y", "Z"})); + params.insertLinkableParameter(std::make_unique(k_RemoveOriginalGeometry_Key, "Perform In Place", "Replaces the original Edge Geometry after filter is completed", true)); + params.insert(std::make_unique(k_BoundaryIntersectionBehavior_Key, "Boundary Intersection Behavior", + "The behavior to implement if an edge intersects a bound (one vertex is inside, one vertex is outside).\n\n\"Interpolate Outside Vertex\" will move " + "the outside vertex of a boundary-intersecting edge from its current position to the boundary edge.\n\"Ignore Edge\" will ignore any edge that " + "intersects a bound.\n\"Filter Error\" will make this filter throw an error when it encounters an edge that intersects a bound.", + to_underlying(CropEdgeGeometry::BoundaryIntersectionBehavior::InterpolateOutsideVertex), + ChoicesParameter::Choices{"Interpolate Outside Vertex", "Ignore Edge", "Filter Error"})); + + params.insertSeparator(Parameters::Separator{"Input Edge Geometry"}); + params.insert( + std::make_unique(k_SelectedEdgeGeometryPath_Key, "Selected Edge Geometry", "DataPath to the source Edge Geometry", DataPath(), std::set{IGeometry::Type::Edge})); + + params.insertSeparator(Parameters::Separator{"Output Edge Geometry"}); + params.insert(std::make_unique(k_CreatedEdgeGeometryPath_Key, "Created Edge Geometry", "The DataPath to store the created Edge Geometry", DataPath())); + + // Associate the Linkable Parameter(s) to the children parameters that they control + params.linkParameters(k_RemoveOriginalGeometry_Key, k_CreatedEdgeGeometryPath_Key, false); + + return params; +} + +//------------------------------------------------------------------------------ +IFilter::VersionType CropEdgeGeometryFilter::parametersVersion() const +{ + return 1; +} + +//------------------------------------------------------------------------------ +IFilter::UniquePointer CropEdgeGeometryFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult CropEdgeGeometryFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) const +{ + auto srcEdgeGeomPath = filterArgs.value(k_SelectedEdgeGeometryPath_Key); + auto destEdgeGeomPath = filterArgs.value(k_CreatedEdgeGeometryPath_Key); + auto minCoords = filterArgs.value>(k_MinCoord_Key); + auto maxCoords = filterArgs.value>(k_MaxCoord_Key); + auto pRemoveOriginalGeometry = filterArgs.value(k_RemoveOriginalGeometry_Key); + auto pCropXDim = filterArgs.value(k_CropXDim_Key); + auto pCropYDim = filterArgs.value(k_CropYDim_Key); + auto pCropZDim = filterArgs.value(k_CropZDim_Key); + + auto& srcEdgeGeom = dataStructure.getDataRefAs(srcEdgeGeomPath); + + if(!pCropXDim && !pCropYDim && !pCropZDim) + { + return {MakeErrorResult(to_underlying(CropEdgeGeometry::ErrorCodes::NoDimensionsChosen), "At least one dimension must be selected to crop!")}; + } + + float32 xMin = pCropXDim ? minCoords[0] : std::numeric_limits::lowest(); + float32 xMax = pCropXDim ? maxCoords[0] : std::numeric_limits::max(); + float32 yMin = pCropYDim ? minCoords[1] : std::numeric_limits::lowest(); + float32 yMax = pCropYDim ? maxCoords[1] : std::numeric_limits::max(); + float32 zMin = pCropZDim ? minCoords[2] : std::numeric_limits::lowest(); + float32 zMax = pCropZDim ? maxCoords[2] : std::numeric_limits::max(); + + nx::core::Result resultOutputActions; + std::vector preflightUpdatedValues; + + if(pCropXDim && xMax < xMin) + { + const std::string errMsg = fmt::format("X Max ({}) less than X Min ({})", xMax, xMin); + return {MakeErrorResult(to_underlying(CropEdgeGeometry::ErrorCodes::XMinLargerThanXMax), errMsg)}; + } + if(pCropYDim && yMax < yMin) + { + const std::string errMsg = fmt::format("Y Max ({}) less than Y Min ({})", yMax, yMin); + return {MakeErrorResult(to_underlying(CropEdgeGeometry::ErrorCodes::YMinLargerThanYMax), errMsg)}; + } + if(pCropZDim && zMax < zMin) + { + const std::string errMsg = fmt::format("Z Max ({}) less than Z Min ({})", zMax, zMin); + return {MakeErrorResult(to_underlying(CropEdgeGeometry::ErrorCodes::ZMinLargerThanZMax), errMsg)}; + } + + std::vector ignorePaths; // already copied over so skip these when collecting child paths to finish copying over later + + if(pRemoveOriginalGeometry) + { + // Generate a new name for the current Edge Geometry + auto tempPathVector = srcEdgeGeomPath.getPathVector(); + std::string tempName = "." + tempPathVector.back(); + tempPathVector.back() = tempName; + DataPath tempPath(tempPathVector); + // Rename the current edge geometry + resultOutputActions.value().appendDeferredAction(std::make_unique(srcEdgeGeomPath, tempName)); + // After the execute function has been done, delete the moved edge geometry + resultOutputActions.value().appendDeferredAction(std::make_unique(tempPath)); + + tempPathVector = srcEdgeGeomPath.getPathVector(); + tempName = k_TempGeometryName; + tempPathVector.back() = tempName; + destEdgeGeomPath = DataPath({tempPathVector}); + } + + // This section gets the cell attribute matrix for the input Edge Geometry and + // then creates new arrays from each array that is in that attribute matrix. We + // also push this attribute matrix into the `ignorePaths` variable since we do + // not need to manually copy these arrays to the destination edge geometry + { + // Get the name of the Edge Attribute Matrix, so we can use that in the CreateEdgeGeometryAction + const auto* srcEdgeGeomPtr = dataStructure.getDataAs(srcEdgeGeomPath); + const AttributeMatrix& selectedVertexData = *srcEdgeGeomPtr->getVertexAttributeMatrix(); + const AttributeMatrix& selectedEdgeData = *srcEdgeGeomPtr->getEdgeAttributeMatrix(); + { + auto& vertexAttrMatrix = srcEdgeGeom.getVertexAttributeMatrixRef(); + auto& edgeAttrMatrix = srcEdgeGeom.getEdgeAttributeMatrixRef(); + auto& verticesArray = srcEdgeGeom.getVerticesRef(); + auto& edgesArray = srcEdgeGeom.getEdgesRef(); + resultOutputActions.value().appendAction( + std::make_unique(destEdgeGeomPath, 1, 1, vertexAttrMatrix.getName(), edgeAttrMatrix.getName(), verticesArray.getName(), edgesArray.getName())); + + auto vertexAttrMatDataPaths = vertexAttrMatrix.getDataPaths(); + auto edgeAttrMatDataPaths = edgeAttrMatrix.getDataPaths(); + auto verticesArrayDataPaths = verticesArray.getDataPaths(); + auto edgesArrayDataPaths = edgesArray.getDataPaths(); + ignorePaths.insert(ignorePaths.end(), vertexAttrMatDataPaths.begin(), vertexAttrMatDataPaths.end()); + ignorePaths.insert(ignorePaths.end(), edgeAttrMatDataPaths.begin(), edgeAttrMatDataPaths.end()); + ignorePaths.insert(ignorePaths.end(), verticesArrayDataPaths.begin(), verticesArrayDataPaths.end()); + ignorePaths.insert(ignorePaths.end(), edgesArrayDataPaths.begin(), edgesArrayDataPaths.end()); + } + + // Now loop over each array in the source edge geometry's cell attribute matrix and create the corresponding arrays + // in the destination edge geometry's cell attribute matrix + DataPath newEdgeAttributeMatrixPath = destEdgeGeomPath.createChildPath(selectedEdgeData.getName()); + for(const auto& [identifier, object] : selectedEdgeData) + { + const auto& srcArray = dynamic_cast(*object); + DataType dataType = srcArray.getDataType(); + IDataStore::ShapeType componentShape = srcArray.getIDataStoreRef().getComponentShape(); + DataPath dataArrayPath = newEdgeAttributeMatrixPath.createChildPath(srcArray.getName()); + resultOutputActions.value().appendAction(std::make_unique(dataType, std::vector{1}, std::move(componentShape), dataArrayPath)); + } + + // Now loop over each array in the source edge geometry's vertex attribute matrix and create the corresponding arrays + // in the destination edge geometry's vertex attribute matrix + DataPath newVertexAttributeMatrixPath = destEdgeGeomPath.createChildPath(selectedVertexData.getName()); + + auto vertexArraysResult = selectedVertexData.findAllChildrenOfType(); + if(!vertexArraysResult.empty()) + { + // Detected at least one vertex array, throw a warning + resultOutputActions.warnings().push_back( + {-100, "A vertex data array was detected in the selected edge geometry. This filter currently only interpolates vertex positions, associated vertex data values will not be interpolated."}); + } + + for(const auto& [identifier, object] : selectedVertexData) + { + const auto& srcArray = dynamic_cast(*object); + DataType dataType = srcArray.getDataType(); + IDataStore::ShapeType componentShape = srcArray.getIDataStoreRef().getComponentShape(); + DataPath dataArrayPath = newVertexAttributeMatrixPath.createChildPath(srcArray.getName()); + resultOutputActions.value().appendAction(std::make_unique(dataType, std::vector{1}, std::move(componentShape), dataArrayPath)); + } + + // Store the preflight updated value(s) into the preflightUpdatedValues vector using the appropriate methods. + std::string cropOptionsStr = "This filter will crop the edge geometry in the following dimension(s): "; + cropOptionsStr.append(pCropXDim ? "X" : ""); + cropOptionsStr.append(pCropYDim ? "Y" : ""); + cropOptionsStr.append(pCropZDim ? "Z" : ""); + preflightUpdatedValues.push_back({"Crop Dimensions", cropOptionsStr}); + } + + // This section covers copying the other Attribute Matrix objects from the source geometry + // to the destination geometry + + auto childPaths = GetAllChildDataPaths(dataStructure, srcEdgeGeomPath, DataObject::Type::DataObject, ignorePaths); + if(childPaths.has_value()) + { + for(const auto& childPath : childPaths.value()) + { + std::string copiedChildName = nx::core::StringUtilities::replace(childPath.toString(), srcEdgeGeomPath.getTargetName(), destEdgeGeomPath.getTargetName()); + DataPath copiedChildPath = DataPath::FromString(copiedChildName).value(); + if(dataStructure.getDataAs(childPath) != nullptr) + { + std::vector allCreatedPaths = {copiedChildPath}; + auto pathsToBeCopied = GetAllChildDataPathsRecursive(dataStructure, childPath); + if(pathsToBeCopied.has_value()) + { + for(const auto& sourcePath : pathsToBeCopied.value()) + { + std::string createdPathName = nx::core::StringUtilities::replace(sourcePath.toString(), srcEdgeGeomPath.getTargetName(), destEdgeGeomPath.getTargetName()); + allCreatedPaths.push_back(DataPath::FromString(createdPathName).value()); + } + } + resultOutputActions.value().appendAction(std::make_unique(childPath, copiedChildPath, allCreatedPaths)); + } + else + { + resultOutputActions.value().appendAction(std::make_unique(childPath, copiedChildPath, std::vector{copiedChildPath})); + } + } + } + + if(pRemoveOriginalGeometry) + { + resultOutputActions.value().appendDeferredAction(std::make_unique(destEdgeGeomPath, srcEdgeGeomPath.getTargetName())); + } + + // Return both the resultOutputActions and the preflightUpdatedValues via std::move() + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} + +//------------------------------------------------------------------------------ +Result<> CropEdgeGeometryFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) const +{ + CropEdgeGeometryInputValues inputValues; + + inputValues.srcEdgeGeomPath = filterArgs.value(k_SelectedEdgeGeometryPath_Key); + inputValues.destEdgeGeomPath = filterArgs.value(k_CreatedEdgeGeometryPath_Key); + inputValues.minCoords = filterArgs.value>(k_MinCoord_Key); + inputValues.maxCoords = filterArgs.value>(k_MaxCoord_Key); + inputValues.removeOriginalGeometry = filterArgs.value(k_RemoveOriginalGeometry_Key); + inputValues.cropXDim = filterArgs.value(k_CropXDim_Key); + inputValues.cropYDim = filterArgs.value(k_CropYDim_Key); + inputValues.cropZDim = filterArgs.value(k_CropZDim_Key); + inputValues.boundaryIntersectionBehavior = filterArgs.value(k_BoundaryIntersectionBehavior_Key); + + return CropEdgeGeometry(dataStructure, messageHandler, shouldCancel, &inputValues)(); +} diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CropEdgeGeometryFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CropEdgeGeometryFilter.hpp new file mode 100644 index 0000000000..662383a1f7 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CropEdgeGeometryFilter.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/Common/StringLiteral.hpp" +#include "simplnx/Filter/FilterTraits.hpp" +#include "simplnx/Filter/IFilter.hpp" + +namespace nx::core +{ +class SIMPLNXCORE_EXPORT CropEdgeGeometryFilter : public IFilter +{ +public: + CropEdgeGeometryFilter(); + ~CropEdgeGeometryFilter() noexcept override; + + CropEdgeGeometryFilter(const CropEdgeGeometryFilter&) = delete; + CropEdgeGeometryFilter(CropEdgeGeometryFilter&&) noexcept = delete; + + CropEdgeGeometryFilter& operator=(const CropEdgeGeometryFilter&) = delete; + CropEdgeGeometryFilter& operator=(CropEdgeGeometryFilter&&) noexcept = delete; + + // Parameter Keys + static inline constexpr StringLiteral k_CropXDim_Key = "crop_x_dim"; + static inline constexpr StringLiteral k_CropYDim_Key = "crop_y_dim"; + static inline constexpr StringLiteral k_CropZDim_Key = "crop_z_dim"; + static inline constexpr StringLiteral k_MinCoord_Key = "min_coord"; + static inline constexpr StringLiteral k_MaxCoord_Key = "max_coord"; + static inline constexpr StringLiteral k_SelectedEdgeGeometryPath_Key = "input_image_geometry_path"; + static inline constexpr StringLiteral k_CreatedEdgeGeometryPath_Key = "output_image_geometry_path"; + static inline constexpr StringLiteral k_RemoveOriginalGeometry_Key = "remove_original_geometry"; + static inline constexpr StringLiteral k_BoundaryIntersectionBehavior_Key = "boundary_intersection_behavior_index"; + + /** + * @brief + * @return std::string + */ + std::string name() const override; + + /** + * @brief Returns the C++ classname of this filter. + * @return std::string + */ + std::string className() const override; + + /** + * @brief + * @return Uuid + */ + Uuid uuid() const override; + + /** + * @brief + * @return std::string + */ + std::string humanName() const override; + + /** + * @brief Returns the default tags for this filter. + * @return std::vector + */ + std::vector defaultTags() const override; + + /** + * @brief + * @return Parameters + */ + 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 + * @return UniquePointer + */ + UniquePointer clone() const override; + +protected: + /** + * @brief + * @param dataStructure + * @param filterArgs + * @param messageHandler + * @param shouldCancel + * @return PreflightResult + */ + PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) const override; + + /** + * @brief + * @param dataStructure + * @param args + * @param pipelineNode + * @param messageHandler + * @param shouldCancel + * @return Result<> + */ + Result<> executeImpl(DataStructure& dataStructure, const Arguments& args, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) const override; + +private: + int32 m_InstanceId; +}; +} // namespace nx::core + +SIMPLNX_DEF_FILTER_TRAITS(nx::core, CropEdgeGeometryFilter, "e3e794c9-ef0c-4ed2-a25f-8d19b6b2ce4c"); diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index 3205426c19..5ae4af0332 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -58,6 +58,7 @@ set(${PLUGIN_NAME}UnitTest_SRCS CreateGeometryTest.cpp CreateImageGeometryTest.cpp CreatePythonSkeletonTest.cpp + CropEdgeGeometryTest.cpp CropImageGeometryTest.cpp CropVertexGeometryTest.cpp DBSCANTest.cpp diff --git a/src/Plugins/SimplnxCore/test/CropEdgeGeometryTest.cpp b/src/Plugins/SimplnxCore/test/CropEdgeGeometryTest.cpp new file mode 100644 index 0000000000..b8f7a53fae --- /dev/null +++ b/src/Plugins/SimplnxCore/test/CropEdgeGeometryTest.cpp @@ -0,0 +1,341 @@ +#include "SimplnxCore/Filters/Algorithms/CropEdgeGeometry.hpp" +#include "SimplnxCore/Filters/CropEdgeGeometryFilter.hpp" +#include "SimplnxCore/SimplnxCore_test_dirs.hpp" + +#include "simplnx/Common/StringLiteral.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/UnitTest/UnitTestCommon.hpp" +#include + +#include + +using namespace nx::core; + +namespace +{ +inline constexpr StringLiteral k_EdgeGeometry("Edge Geometry"); +inline constexpr StringLiteral k_CroppedEdgeGeometry("Cropped Edge Geometry"); +inline constexpr StringLiteral k_VerticesName("Vertices"); +inline constexpr StringLiteral k_EdgesName("Edges"); +inline constexpr StringLiteral k_UInt32ArrayName("UInt32Array"); +inline constexpr StringLiteral k_Int32ArrayName("Int32Array"); +inline constexpr StringLiteral k_Int64ArrayName("Int64Array"); +const DataPath k_UInt32ArrayPath = DataPath({k_EdgeGeometry}).createChildPath(Constants::k_VertexData).createChildPath(k_UInt32ArrayName); +const DataPath k_Int32ArrayPath = DataPath({k_EdgeGeometry}).createChildPath(Constants::k_Edge_Data).createChildPath(k_Int32ArrayName); +const DataPath k_Int64ArrayPath = DataPath({k_EdgeGeometry}).createChildPath(Constants::k_Edge_Data).createChildPath(k_Int64ArrayName); + +DataStructure CreateDataStructure() +{ + DataStructure dataStructure; + EdgeGeom* edgeGeom = EdgeGeom::Create(dataStructure, k_EdgeGeometry); + + Float32Array* vertices = UnitTest::CreateTestDataArray(dataStructure, k_VerticesName, {4}, {3}, edgeGeom->getId()); + auto& verticesRef = vertices->getDataStoreRef(); + verticesRef[0] = 0; + verticesRef[1] = 0; + verticesRef[2] = 0; + verticesRef[3] = 1; + verticesRef[4] = 2; + verticesRef[5] = -2; + verticesRef[6] = 3; + verticesRef[7] = 1; + verticesRef[8] = -2; + verticesRef[9] = 2; + verticesRef[10] = -1; + verticesRef[11] = 0; + edgeGeom->setVertices(*vertices); + + IGeometry::SharedEdgeList* edges = UnitTest::CreateTestDataArray(dataStructure, k_EdgesName, {4}, {2}, edgeGeom->getId()); + auto& edgesRef = edges->getDataStoreRef(); + edgesRef[0] = 0; + edgesRef[1] = 1; + edgesRef[2] = 1; + edgesRef[3] = 2; + edgesRef[4] = 2; + edgesRef[5] = 3; + edgesRef[6] = 3; + edgesRef[7] = 0; + edgeGeom->setEdgeList(*edges); + + auto* vertexDataPtr = AttributeMatrix::Create(dataStructure, Constants::k_VertexData, {4}, edgeGeom->getId()); + edgeGeom->setVertexAttributeMatrix(*vertexDataPtr); + + auto* edgeDataPtr = AttributeMatrix::Create(dataStructure, Constants::k_Edge_Data, {4}, edgeGeom->getId()); + edgeGeom->setEdgeAttributeMatrix(*edgeDataPtr); + + UInt32Array* uint32Array = UnitTest::CreateTestDataArray(dataStructure, "UInt32Array", {4}, {1}, vertexDataPtr->getId()); + auto& uint32ArrayRef = uint32Array->getDataStoreRef(); + uint32ArrayRef[0] = 8; + uint32ArrayRef[1] = 3; + uint32ArrayRef[2] = 893; + uint32ArrayRef[3] = 327; + + Int32Array* int32Array = UnitTest::CreateTestDataArray(dataStructure, "Int32Array", {4}, {1}, edgeDataPtr->getId()); + auto& int32ArrayRef = int32Array->getDataStoreRef(); + int32ArrayRef[0] = 12; + int32ArrayRef[1] = 56; + int32ArrayRef[2] = 2; + int32ArrayRef[3] = 91; + + Int64Array* int64Array = UnitTest::CreateTestDataArray(dataStructure, "Int64Array", {4}, {1}, edgeDataPtr->getId()); + auto& int64ArrayRef = int64Array->getDataStoreRef(); + int64ArrayRef[0] = 24; + int64ArrayRef[1] = 124; + int64ArrayRef[2] = 352; + int64ArrayRef[3] = 786; + + return dataStructure; +} +} // namespace + +TEST_CASE("SimplnxCore::CropEdgeGeometryFilter - Filter Error", "[SimplnxCore][CropEdgeGeometryFilter]") +{ + fs::path vertexCoordsPath = fs::path(std::string(nx::core::unit_test::k_BuildDir)) / "Data" / "Test_Data" / "VertexCoordinates.csv"; + fs::path edgeConnectivityPath = fs::path(std::string(nx::core::unit_test::k_BuildDir)) / "Data" / "Test_Data" / "EdgeConnectivity.csv"; + + CropEdgeGeometryFilter filter; + DataStructure dataStructure = CreateDataStructure(); + Arguments args; + + const std::vector k_MinCoords{0.5, 0.5, -0.5}; + const std::vector k_MaxCoords{1.5, 2, 0.5}; + + SECTION("X") + { + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(false)); + } + SECTION("Y") + { + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(false)); + } + SECTION("Z") + { + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(true)); + } + + args.insert(CropEdgeGeometryFilter::k_MinCoord_Key, std::make_any>(k_MinCoords)); + args.insert(CropEdgeGeometryFilter::k_MaxCoord_Key, std::make_any>(k_MaxCoords)); + args.insert(CropEdgeGeometryFilter::k_RemoveOriginalGeometry_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_BoundaryIntersectionBehavior_Key, std::make_any(to_underlying(CropEdgeGeometry::BoundaryIntersectionBehavior::FilterError))); + args.insert(CropEdgeGeometryFilter::k_SelectedEdgeGeometryPath_Key, std::make_any(DataPath({k_EdgeGeometry}))); + args.insert(CropEdgeGeometryFilter::k_CreatedEdgeGeometryPath_Key, std::make_any(DataPath({k_CroppedEdgeGeometry}))); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto result = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(result.result); + REQUIRE(result.result.errors().size() == 1); + REQUIRE(result.result.errors()[0].code == to_underlying(CropEdgeGeometry::ErrorCodes::OutsideVertexError)); +} + +TEST_CASE("SimplnxCore::CropEdgeGeometryFilter - Ignore Edges", "[SimplnxCore][CropEdgeGeometryFilter]") +{ + fs::path vertexCoordsPath = fs::path(std::string(nx::core::unit_test::k_BuildDir)) / "Data" / "Test_Data" / "VertexCoordinates.csv"; + fs::path edgeConnectivityPath = fs::path(std::string(nx::core::unit_test::k_BuildDir)) / "Data" / "Test_Data" / "EdgeConnectivity.csv"; + + CropEdgeGeometryFilter filter; + DataStructure dataStructure = CreateDataStructure(); + Arguments args; + + const std::vector k_MinCoords{-0.5, -0.5, -0.5}; + const std::vector k_MaxCoords{1.5, 2.5, 0.5}; + + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_MinCoord_Key, std::make_any>(k_MinCoords)); + args.insert(CropEdgeGeometryFilter::k_MaxCoord_Key, std::make_any>(k_MaxCoords)); + args.insert(CropEdgeGeometryFilter::k_RemoveOriginalGeometry_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_BoundaryIntersectionBehavior_Key, std::make_any(to_underlying(CropEdgeGeometry::BoundaryIntersectionBehavior::IgnoreEdge))); + args.insert(CropEdgeGeometryFilter::k_SelectedEdgeGeometryPath_Key, std::make_any(DataPath({k_EdgeGeometry}))); + args.insert(CropEdgeGeometryFilter::k_CreatedEdgeGeometryPath_Key, std::make_any(DataPath({k_CroppedEdgeGeometry}))); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto result = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(result.result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_EdgeGeometry}))); + auto& edgeGeom = dataStructure.getDataRefAs(DataPath({k_EdgeGeometry})); + Float32Array& vertices = edgeGeom.getVerticesRef(); + UInt64Array& edges = edgeGeom.getEdgesRef(); + REQUIRE(vertices.getNumberOfTuples() == 2); + REQUIRE(edges.getNumberOfTuples() == 1); + REQUIRE(vertices[0] == 0); + REQUIRE(vertices[1] == 0); + REQUIRE(vertices[2] == 0); + REQUIRE(vertices[3] == 1); + REQUIRE(vertices[4] == 2); + REQUIRE(vertices[5] == -2); + REQUIRE(edges[0] == 0); + REQUIRE(edges[1] == 1); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_UInt32ArrayPath)); + auto& uint32Array = dataStructure.getDataRefAs(k_UInt32ArrayPath); + REQUIRE(uint32Array.getNumberOfTuples() == 2); + REQUIRE(uint32Array[0] == 8); + REQUIRE(uint32Array[1] == 3); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_Int32ArrayPath)); + auto& int32Array = dataStructure.getDataRefAs(k_Int32ArrayPath); + REQUIRE(int32Array.getNumberOfTuples() == 1); + REQUIRE(int32Array[0] == 12); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_Int64ArrayPath)); + auto& int64Array = dataStructure.getDataRefAs(k_Int64ArrayPath); + REQUIRE(int64Array.getNumberOfTuples() == 1); + REQUIRE(int64Array[0] == 24); +} + +TEST_CASE("SimplnxCore::CropEdgeGeometryFilter - Interpolate Outside Vertices", "[SimplnxCore][CropEdgeGeometryFilter]") +{ + fs::path vertexCoordsPath = fs::path(std::string(nx::core::unit_test::k_BuildDir)) / "Data" / "Test_Data" / "VertexCoordinates.csv"; + fs::path edgeConnectivityPath = fs::path(std::string(nx::core::unit_test::k_BuildDir)) / "Data" / "Test_Data" / "EdgeConnectivity.csv"; + + CropEdgeGeometryFilter filter; + DataStructure dataStructure = CreateDataStructure(); + Arguments args; + + const std::vector k_MinCoords{-0.5, -0.5, -0.5}; + const std::vector k_MaxCoords{1.5, 2.5, 0.5}; + + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_MinCoord_Key, std::make_any>(k_MinCoords)); + args.insert(CropEdgeGeometryFilter::k_MaxCoord_Key, std::make_any>(k_MaxCoords)); + args.insert(CropEdgeGeometryFilter::k_RemoveOriginalGeometry_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_BoundaryIntersectionBehavior_Key, + std::make_any(to_underlying(CropEdgeGeometry::BoundaryIntersectionBehavior::InterpolateOutsideVertex))); + args.insert(CropEdgeGeometryFilter::k_SelectedEdgeGeometryPath_Key, std::make_any(DataPath({k_EdgeGeometry}))); + args.insert(CropEdgeGeometryFilter::k_CreatedEdgeGeometryPath_Key, std::make_any(DataPath({k_CroppedEdgeGeometry}))); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto result = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(result.result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({k_EdgeGeometry}))); + auto& edgeGeom = dataStructure.getDataRefAs(DataPath({k_EdgeGeometry})); + Float32Array& vertices = edgeGeom.getVerticesRef(); + UInt64Array& edges = edgeGeom.getEdgesRef(); + REQUIRE(vertices.getNumberOfTuples() == 4); + REQUIRE(edges.getNumberOfTuples() == 3); + REQUIRE(vertices[0] == 0); + REQUIRE(vertices[1] == 0); + REQUIRE(vertices[2] == 0); + REQUIRE(vertices[3] == 1); + REQUIRE(vertices[4] == 2); + REQUIRE(vertices[5] == -2); + REQUIRE(vertices[6] == 1.5); + REQUIRE(vertices[7] == 1.75); + REQUIRE(vertices[8] == -2); + REQUIRE(vertices[9] == 1); + REQUIRE(vertices[10] == -0.5); + REQUIRE(vertices[11] == 0); + REQUIRE(edges[0] == 0); + REQUIRE(edges[1] == 1); + REQUIRE(edges[2] == 1); + REQUIRE(edges[3] == 2); + REQUIRE(edges[4] == 3); + REQUIRE(edges[5] == 0); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_UInt32ArrayPath)); + auto& uint32Array = dataStructure.getDataRefAs(k_UInt32ArrayPath); + REQUIRE(uint32Array.getNumberOfTuples() == 4); + REQUIRE(uint32Array[0] == 8); + REQUIRE(uint32Array[1] == 3); + REQUIRE(uint32Array[2] == 893); + REQUIRE(uint32Array[3] == 327); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_Int32ArrayPath)); + auto& int32Array = dataStructure.getDataRefAs(k_Int32ArrayPath); + REQUIRE(int32Array.getNumberOfTuples() == 3); + REQUIRE(int32Array[0] == 12); + REQUIRE(int32Array[1] == 56); + REQUIRE(int32Array[2] == 91); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_Int64ArrayPath)); + auto& int64Array = dataStructure.getDataRefAs(k_Int64ArrayPath); + REQUIRE(int64Array.getNumberOfTuples() == 3); + REQUIRE(int64Array[0] == 24); + REQUIRE(int64Array[1] == 124); + REQUIRE(int64Array[2] == 786); +} + +TEST_CASE("SimplnxCore::CropEdgeGeometryFilter - Invalid Params", "[SimplnxCore][CropEdgeGeometryFilter]") +{ + fs::path vertexCoordsPath = fs::path(std::string(nx::core::unit_test::k_BuildDir)) / "Data" / "Test_Data" / "VertexCoordinates.csv"; + fs::path edgeConnectivityPath = fs::path(std::string(nx::core::unit_test::k_BuildDir)) / "Data" / "Test_Data" / "EdgeConnectivity.csv"; + + CropEdgeGeometryFilter filter; + DataStructure dataStructure = CreateDataStructure(); + Arguments args; + + std::vector minCoords; + std::vector maxCoords; + int64 errCode; + + SECTION("X Min > X Max") + { + minCoords = {2.5, -0.5, -0.5}; + maxCoords = {1.5, 2.5, 0.5}; + errCode = to_underlying(CropEdgeGeometry::ErrorCodes::XMinLargerThanXMax); + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(false)); + } + SECTION("Y Min > Y Max") + { + minCoords = {-0.5, 3.5, -0.5}; + maxCoords = {1.5, 2.5, 0.5}; + errCode = to_underlying(CropEdgeGeometry::ErrorCodes::YMinLargerThanYMax); + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(false)); + } + SECTION("Z Min > Z Max") + { + minCoords = {-0.5, -0.5, 1.0}; + maxCoords = {1.5, 2.5, 0.5}; + errCode = to_underlying(CropEdgeGeometry::ErrorCodes::ZMinLargerThanZMax); + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(true)); + } + SECTION("No dimensions chosen") + { + minCoords = {-0.5, -0.5, -0.5}; + maxCoords = {1.5, 2.5, 0.5}; + errCode = to_underlying(CropEdgeGeometry::ErrorCodes::NoDimensionsChosen); + args.insert(CropEdgeGeometryFilter::k_CropXDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropYDim_Key, std::make_any(false)); + args.insert(CropEdgeGeometryFilter::k_CropZDim_Key, std::make_any(false)); + } + + args.insert(CropEdgeGeometryFilter::k_MinCoord_Key, std::make_any>(minCoords)); + args.insert(CropEdgeGeometryFilter::k_MaxCoord_Key, std::make_any>(maxCoords)); + args.insert(CropEdgeGeometryFilter::k_RemoveOriginalGeometry_Key, std::make_any(true)); + args.insert(CropEdgeGeometryFilter::k_BoundaryIntersectionBehavior_Key, + std::make_any(to_underlying(CropEdgeGeometry::BoundaryIntersectionBehavior::InterpolateOutsideVertex))); + args.insert(CropEdgeGeometryFilter::k_SelectedEdgeGeometryPath_Key, std::make_any(DataPath({k_EdgeGeometry}))); + args.insert(CropEdgeGeometryFilter::k_CreatedEdgeGeometryPath_Key, std::make_any(DataPath({k_CroppedEdgeGeometry}))); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) + REQUIRE(preflightResult.outputActions.errors().size() == 1); + REQUIRE(preflightResult.outputActions.errors()[0].code == errCode); +}