From 750bae47f67f53321ded2412caeddffb086d8024 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 14:08:54 -0700 Subject: [PATCH 01/15] Moved files --- src/axom/quest/{readers => io}/C2CReader.cpp | 0 src/axom/quest/{readers => io}/C2CReader.hpp | 0 src/axom/quest/{readers => io}/PC2CReader.cpp | 0 src/axom/quest/{readers => io}/PC2CReader.hpp | 0 src/axom/quest/{readers => io}/PProEReader.cpp | 0 src/axom/quest/{readers => io}/PProEReader.hpp | 0 src/axom/quest/{readers => io}/PSTLReader.cpp | 0 src/axom/quest/{readers => io}/PSTLReader.hpp | 0 src/axom/quest/{readers => io}/ProEReader.cpp | 0 src/axom/quest/{readers => io}/ProEReader.hpp | 0 src/axom/quest/{readers => io}/STLReader.cpp | 0 src/axom/quest/{readers => io}/STLReader.hpp | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename src/axom/quest/{readers => io}/C2CReader.cpp (100%) rename src/axom/quest/{readers => io}/C2CReader.hpp (100%) rename src/axom/quest/{readers => io}/PC2CReader.cpp (100%) rename src/axom/quest/{readers => io}/PC2CReader.hpp (100%) rename src/axom/quest/{readers => io}/PProEReader.cpp (100%) rename src/axom/quest/{readers => io}/PProEReader.hpp (100%) rename src/axom/quest/{readers => io}/PSTLReader.cpp (100%) rename src/axom/quest/{readers => io}/PSTLReader.hpp (100%) rename src/axom/quest/{readers => io}/ProEReader.cpp (100%) rename src/axom/quest/{readers => io}/ProEReader.hpp (100%) rename src/axom/quest/{readers => io}/STLReader.cpp (100%) rename src/axom/quest/{readers => io}/STLReader.hpp (100%) diff --git a/src/axom/quest/readers/C2CReader.cpp b/src/axom/quest/io/C2CReader.cpp similarity index 100% rename from src/axom/quest/readers/C2CReader.cpp rename to src/axom/quest/io/C2CReader.cpp diff --git a/src/axom/quest/readers/C2CReader.hpp b/src/axom/quest/io/C2CReader.hpp similarity index 100% rename from src/axom/quest/readers/C2CReader.hpp rename to src/axom/quest/io/C2CReader.hpp diff --git a/src/axom/quest/readers/PC2CReader.cpp b/src/axom/quest/io/PC2CReader.cpp similarity index 100% rename from src/axom/quest/readers/PC2CReader.cpp rename to src/axom/quest/io/PC2CReader.cpp diff --git a/src/axom/quest/readers/PC2CReader.hpp b/src/axom/quest/io/PC2CReader.hpp similarity index 100% rename from src/axom/quest/readers/PC2CReader.hpp rename to src/axom/quest/io/PC2CReader.hpp diff --git a/src/axom/quest/readers/PProEReader.cpp b/src/axom/quest/io/PProEReader.cpp similarity index 100% rename from src/axom/quest/readers/PProEReader.cpp rename to src/axom/quest/io/PProEReader.cpp diff --git a/src/axom/quest/readers/PProEReader.hpp b/src/axom/quest/io/PProEReader.hpp similarity index 100% rename from src/axom/quest/readers/PProEReader.hpp rename to src/axom/quest/io/PProEReader.hpp diff --git a/src/axom/quest/readers/PSTLReader.cpp b/src/axom/quest/io/PSTLReader.cpp similarity index 100% rename from src/axom/quest/readers/PSTLReader.cpp rename to src/axom/quest/io/PSTLReader.cpp diff --git a/src/axom/quest/readers/PSTLReader.hpp b/src/axom/quest/io/PSTLReader.hpp similarity index 100% rename from src/axom/quest/readers/PSTLReader.hpp rename to src/axom/quest/io/PSTLReader.hpp diff --git a/src/axom/quest/readers/ProEReader.cpp b/src/axom/quest/io/ProEReader.cpp similarity index 100% rename from src/axom/quest/readers/ProEReader.cpp rename to src/axom/quest/io/ProEReader.cpp diff --git a/src/axom/quest/readers/ProEReader.hpp b/src/axom/quest/io/ProEReader.hpp similarity index 100% rename from src/axom/quest/readers/ProEReader.hpp rename to src/axom/quest/io/ProEReader.hpp diff --git a/src/axom/quest/readers/STLReader.cpp b/src/axom/quest/io/STLReader.cpp similarity index 100% rename from src/axom/quest/readers/STLReader.cpp rename to src/axom/quest/io/STLReader.cpp diff --git a/src/axom/quest/readers/STLReader.hpp b/src/axom/quest/io/STLReader.hpp similarity index 100% rename from src/axom/quest/readers/STLReader.hpp rename to src/axom/quest/io/STLReader.hpp From 3a07efe9c7a9e00dcc761c137d3e493d600d0570 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 14:09:31 -0700 Subject: [PATCH 02/15] Updated includes --- RELEASE-NOTES.md | 2 ++ src/axom/quest/CMakeLists.txt | 30 ++++++++++--------- src/axom/quest/examples/quest_proe_bbox.cpp | 2 +- .../quest/interface/internal/QuestHelpers.cpp | 8 ++--- .../quest/interface/internal/QuestHelpers.hpp | 4 +-- src/axom/quest/io/C2CReader.cpp | 2 +- src/axom/quest/io/PC2CReader.cpp | 2 +- src/axom/quest/io/PC2CReader.hpp | 2 +- src/axom/quest/io/PProEReader.cpp | 2 +- src/axom/quest/io/PProEReader.hpp | 2 +- src/axom/quest/io/PSTLReader.cpp | 2 +- src/axom/quest/io/PSTLReader.hpp | 2 +- src/axom/quest/io/ProEReader.cpp | 2 +- src/axom/quest/io/STLReader.cpp | 2 +- src/axom/quest/tests/quest_c2c_reader.cpp | 2 +- src/axom/quest/tests/quest_pro_e_reader.cpp | 2 +- .../tests/quest_pro_e_reader_parallel.cpp | 2 +- src/axom/quest/tests/quest_stl_reader.cpp | 2 +- src/tools/mesh_tester.cpp | 2 +- 19 files changed, 39 insertions(+), 35 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 19208a091d..6ef6c96907 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -42,6 +42,7 @@ The Axom project release numbers follow [Semantic Versioning](http://semver.org/ - Primal: Adds ability to perform sample-based shaping on tetrahedral shapes. - Improves efficiency of volume fraction computation from quadrature samples during sample-based shaping. - Adds a `axom::DeviceHash` type as a GPU-enabled version of the `std::hash` interface. +- Added a new `quest::STLWriter` class that writes mint meshes to STL format. ### Changed - Fixed `Timer::elapsed*()` methods so they properly report the sum of all start/stop cycles @@ -53,6 +54,7 @@ The Axom project release numbers follow [Semantic Versioning](http://semver.org/ - The `primal::BoundingBox` class' `expand()` and `shift()` methods were modified so they do nothing when called on invalid bounding boxes. - Updates to [MFEM version 4.8.0][https://github.com/mfem/mfem/releases/tag/v4.8] +- Readers in Quest were moved from a `quest/readers` directory to `quest/io`. ### Fixed - Core: prevent incorrect instantiations of `axom::Array` from a host-only compile, when Axom is compiled diff --git a/src/axom/quest/CMakeLists.txt b/src/axom/quest/CMakeLists.txt index c51b6a276d..af87eedbfd 100644 --- a/src/axom/quest/CMakeLists.txt +++ b/src/axom/quest/CMakeLists.txt @@ -47,9 +47,10 @@ set( quest_headers detail/PointFinder.hpp detail/PointInCellMeshWrapper_mfem.hpp - ## File readers - readers/STLReader.hpp - readers/ProEReader.hpp + ## File I/O + io/STLReader.hpp + io/STLWriter.hpp + io/ProEReader.hpp ## quest interface interface/internal/mpicomm_wrapper.hpp @@ -75,9 +76,10 @@ set( quest_sources ## Mesh tester MeshTester.cpp - ## File readers - readers/STLReader.cpp - readers/ProEReader.cpp + ## File I/O + io/STLReader.cpp + io/STLWriter.cpp + io/ProEReader.cpp ## quest interface interface/internal/QuestHelpers.cpp @@ -150,19 +152,19 @@ if(AXOM_ENABLE_KLEE AND AXOM_ENABLE_SIDRE) endif() if(C2C_FOUND) - list(APPEND quest_headers readers/C2CReader.hpp) - list(APPEND quest_sources readers/C2CReader.cpp) + list(APPEND quest_headers io/C2CReader.hpp) + list(APPEND quest_sources io/C2CReader.cpp) list(APPEND quest_depends_on c2c) endif() if (AXOM_ENABLE_MPI) - list(APPEND quest_headers readers/PSTLReader.hpp - readers/PProEReader.hpp) - blt_list_append(TO quest_headers IF C2C_FOUND ELEMENTS readers/PC2CReader.hpp) + list(APPEND quest_headers io/PSTLReader.hpp + io/PProEReader.hpp) + blt_list_append(TO quest_headers IF C2C_FOUND ELEMENTS io/PC2CReader.hpp) - list(APPEND quest_sources readers/PSTLReader.cpp - readers/PProEReader.cpp) - blt_list_append(TO quest_sources IF C2C_FOUND ELEMENTS readers/PC2CReader.cpp) + list(APPEND quest_sources io/PSTLReader.cpp + io/PProEReader.cpp) + blt_list_append(TO quest_sources IF C2C_FOUND ELEMENTS io/PC2CReader.cpp) endif() if (SHROUD_FOUND) diff --git a/src/axom/quest/examples/quest_proe_bbox.cpp b/src/axom/quest/examples/quest_proe_bbox.cpp index 621167d833..7b2122ee98 100644 --- a/src/axom/quest/examples/quest_proe_bbox.cpp +++ b/src/axom/quest/examples/quest_proe_bbox.cpp @@ -25,7 +25,7 @@ // _read_proe_include2_end // _read_proe_include1_start -#include "axom/quest/readers/ProEReader.hpp" +#include "axom/quest/io/ProEReader.hpp" // _read_proe_include1_end // _read_proe_typealiases_start diff --git a/src/axom/quest/interface/internal/QuestHelpers.cpp b/src/axom/quest/interface/internal/QuestHelpers.cpp index cf429f72ed..4049749e55 100644 --- a/src/axom/quest/interface/internal/QuestHelpers.cpp +++ b/src/axom/quest/interface/internal/QuestHelpers.cpp @@ -11,15 +11,15 @@ // Quest includes #ifdef AXOM_USE_MPI - #include "axom/quest/readers/PSTLReader.hpp" - #include "axom/quest/readers/PProEReader.hpp" + #include "axom/quest/io/PSTLReader.hpp" + #include "axom/quest/io/PProEReader.hpp" #endif #if defined(AXOM_USE_C2C) #if defined(AXOM_USE_MPI) - #include "axom/quest/readers/PC2CReader.hpp" + #include "axom/quest/io/PC2CReader.hpp" #else - #include "axom/quest/readers/C2CReader.hpp" + #include "axom/quest/io/C2CReader.hpp" #endif #endif diff --git a/src/axom/quest/interface/internal/QuestHelpers.hpp b/src/axom/quest/interface/internal/QuestHelpers.hpp index 5851c25bf5..8fe5da0760 100644 --- a/src/axom/quest/interface/internal/QuestHelpers.hpp +++ b/src/axom/quest/interface/internal/QuestHelpers.hpp @@ -11,8 +11,8 @@ #include "axom/slic.hpp" #include "axom/mint/mesh/Mesh.hpp" #include "axom/quest/interface/internal/mpicomm_wrapper.hpp" -#include "axom/quest/readers/STLReader.hpp" -#include "axom/quest/readers/ProEReader.hpp" +#include "axom/quest/io/STLReader.hpp" +#include "axom/quest/io/ProEReader.hpp" // C/C++ includes #include diff --git a/src/axom/quest/io/C2CReader.cpp b/src/axom/quest/io/C2CReader.cpp index 3ba3a67c09..2e1084d1ba 100644 --- a/src/axom/quest/io/C2CReader.cpp +++ b/src/axom/quest/io/C2CReader.cpp @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "axom/quest/readers/C2CReader.hpp" +#include "axom/quest/io/C2CReader.hpp" #ifndef AXOM_USE_C2C #error C2CReader should only be included when Axom is configured with C2C diff --git a/src/axom/quest/io/PC2CReader.cpp b/src/axom/quest/io/PC2CReader.cpp index 4f0421db98..5c5e5baad6 100644 --- a/src/axom/quest/io/PC2CReader.cpp +++ b/src/axom/quest/io/PC2CReader.cpp @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "axom/quest/readers/PC2CReader.hpp" +#include "axom/quest/io/PC2CReader.hpp" #ifndef AXOM_USE_C2C #error PC2CReader should only be included when Axom is configured with C2C diff --git a/src/axom/quest/io/PC2CReader.hpp b/src/axom/quest/io/PC2CReader.hpp index 5ee3d87e9f..d129d52047 100644 --- a/src/axom/quest/io/PC2CReader.hpp +++ b/src/axom/quest/io/PC2CReader.hpp @@ -13,7 +13,7 @@ #endif #include "axom/core/Macros.hpp" -#include "axom/quest/readers/C2CReader.hpp" // base class +#include "axom/quest/io/C2CReader.hpp" // base class #include "mpi.h" diff --git a/src/axom/quest/io/PProEReader.cpp b/src/axom/quest/io/PProEReader.cpp index 3671bff282..3e86d34e85 100644 --- a/src/axom/quest/io/PProEReader.cpp +++ b/src/axom/quest/io/PProEReader.cpp @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "axom/quest/readers/PProEReader.hpp" +#include "axom/quest/io/PProEReader.hpp" namespace axom { diff --git a/src/axom/quest/io/PProEReader.hpp b/src/axom/quest/io/PProEReader.hpp index 234ce397f2..9c733ad627 100644 --- a/src/axom/quest/io/PProEReader.hpp +++ b/src/axom/quest/io/PProEReader.hpp @@ -8,7 +8,7 @@ #include "axom/config.hpp" #include "axom/core/Macros.hpp" -#include "axom/quest/readers/ProEReader.hpp" // base class +#include "axom/quest/io/ProEReader.hpp" // base class #include "mpi.h" diff --git a/src/axom/quest/io/PSTLReader.cpp b/src/axom/quest/io/PSTLReader.cpp index a569beb6a1..95ed34c61d 100644 --- a/src/axom/quest/io/PSTLReader.cpp +++ b/src/axom/quest/io/PSTLReader.cpp @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "axom/quest/readers/PSTLReader.hpp" +#include "axom/quest/io/PSTLReader.hpp" namespace axom { diff --git a/src/axom/quest/io/PSTLReader.hpp b/src/axom/quest/io/PSTLReader.hpp index 602a8b9d42..ef902b88c2 100644 --- a/src/axom/quest/io/PSTLReader.hpp +++ b/src/axom/quest/io/PSTLReader.hpp @@ -8,7 +8,7 @@ #include "axom/config.hpp" #include "axom/core/Macros.hpp" -#include "axom/quest/readers/STLReader.hpp" // base class +#include "axom/quest/io/STLReader.hpp" // base class #include "mpi.h" diff --git a/src/axom/quest/io/ProEReader.cpp b/src/axom/quest/io/ProEReader.cpp index e9540e4674..d053657f17 100644 --- a/src/axom/quest/io/ProEReader.cpp +++ b/src/axom/quest/io/ProEReader.cpp @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "axom/quest/readers/ProEReader.hpp" +#include "axom/quest/io/ProEReader.hpp" // Axom includes #include "axom/core/utilities/Utilities.hpp" diff --git a/src/axom/quest/io/STLReader.cpp b/src/axom/quest/io/STLReader.cpp index 6094cb4732..ee906192d9 100644 --- a/src/axom/quest/io/STLReader.cpp +++ b/src/axom/quest/io/STLReader.cpp @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "axom/quest/readers/STLReader.hpp" +#include "axom/quest/io/STLReader.hpp" // Axom includes #include "axom/core/utilities/Utilities.hpp" diff --git a/src/axom/quest/tests/quest_c2c_reader.cpp b/src/axom/quest/tests/quest_c2c_reader.cpp index c540cbccb8..3e4517ce73 100644 --- a/src/axom/quest/tests/quest_c2c_reader.cpp +++ b/src/axom/quest/tests/quest_c2c_reader.cpp @@ -9,7 +9,7 @@ #error These tests should only be included when Axom is configured with C2C #endif -#include "axom/quest/readers/C2CReader.hpp" +#include "axom/quest/io/C2CReader.hpp" #include "axom/slic.hpp" #include "axom/mint.hpp" #include "axom/primal.hpp" diff --git a/src/axom/quest/tests/quest_pro_e_reader.cpp b/src/axom/quest/tests/quest_pro_e_reader.cpp index 6b7e8f4a56..34d4e810cf 100644 --- a/src/axom/quest/tests/quest_pro_e_reader.cpp +++ b/src/axom/quest/tests/quest_pro_e_reader.cpp @@ -6,7 +6,7 @@ #include "axom/core/utilities/FileUtilities.hpp" #include "axom/core/NumericLimits.hpp" #include "axom/mint/utils/vtk_utils.hpp" // for write_vtk -#include "axom/quest/readers/ProEReader.hpp" +#include "axom/quest/io/ProEReader.hpp" #include "axom/slic.hpp" // gtest includes diff --git a/src/axom/quest/tests/quest_pro_e_reader_parallel.cpp b/src/axom/quest/tests/quest_pro_e_reader_parallel.cpp index 8b77b588d2..10eb480096 100644 --- a/src/axom/quest/tests/quest_pro_e_reader_parallel.cpp +++ b/src/axom/quest/tests/quest_pro_e_reader_parallel.cpp @@ -9,7 +9,7 @@ #include "axom/slic.hpp" -#include "axom/quest/readers/PProEReader.hpp" +#include "axom/quest/io/PProEReader.hpp" #include "gtest/gtest.h" diff --git a/src/axom/quest/tests/quest_stl_reader.cpp b/src/axom/quest/tests/quest_stl_reader.cpp index 09021afa0c..9bfbe7b290 100644 --- a/src/axom/quest/tests/quest_stl_reader.cpp +++ b/src/axom/quest/tests/quest_stl_reader.cpp @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "axom/quest/readers/STLReader.hpp" +#include "axom/quest/io/STLReader.hpp" #include "axom/mint/mesh/UnstructuredMesh.hpp" #include "axom/slic.hpp" #include "axom/core/NumericLimits.hpp" diff --git a/src/tools/mesh_tester.cpp b/src/tools/mesh_tester.cpp index 6b8684116f..165365c5d3 100644 --- a/src/tools/mesh_tester.cpp +++ b/src/tools/mesh_tester.cpp @@ -20,7 +20,7 @@ #include "axom/spin/UniformGrid.hpp" // _read_stl_include1_start -#include "axom/quest/readers/STLReader.hpp" +#include "axom/quest/io/STLReader.hpp" // _read_stl_include1_end // _check_repair_include_start #include "axom/quest/MeshTester.hpp" From 2a6a93f85031d362dac25d740862bb95574c38e7 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 14:09:42 -0700 Subject: [PATCH 03/15] STLWriter --- src/axom/quest/io/STLWriter.cpp | 154 ++++++++++++++++++++++++++++++++ src/axom/quest/io/STLWriter.hpp | 78 ++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 src/axom/quest/io/STLWriter.cpp create mode 100644 src/axom/quest/io/STLWriter.hpp diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp new file mode 100644 index 0000000000..84f5b7865a --- /dev/null +++ b/src/axom/quest/io/STLWriter.cpp @@ -0,0 +1,154 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) +#include "axom/quest/io/STLWriter.hpp" + +#include "axom/core/execution/reductions.hpp" +#include "axom/mint/mesh/Mesh.hpp" +#include "axom/mint/execution/interface.hpp" +#include "axom/primal/geometry/Vector.hpp" + +#include + +namespace axom +{ +namespace quest +{ + +STLWriter::STLWriter(const std::string &filename, bool binary) : m_fileName(filename), m_binary(binary) +{ +} + +int STLWriter::write(const mint::Mesh* mesh) +{ + SLIC_ERROR_IF(mesh == nullptr, "mesh pointer is null!"); + SLIC_ERROR_IF(m_fileName.length() <= 0, "STL filename is empty!"); + SLIC_ERROR_IF(mesh->getDimension() == 2 || mesh->getDimension() == 3, "Input mesh must is not 2D/3D."); + + std::ofstream out(m_fileName.c_str(), m_binary ? std::ofstream::binary : std::ofstream::out); + if(!out.is_open()) + { + SLIC_WARNING("cannot write to file [" << m_fileName << "]"); + return -1; + } + + // Write header + if(m_binary) + { + std::uint8_t header[80] = {}; + strcpy(reinterpret_cast(header), "STL Binary Header Written by Axom"); + out.write(reinterpret_cast(header), 80); + + // Compute the number of triangles we'll write. + axom::ReduceSum ntri_reduce(0); + axom::mint::for_all_faces( + mesh, + AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType* AXOM_UNUSED_PARAM(nodes), IndexType N) { + ntri_reduce += static_cast(N - 2); + }); + + // Write number of triangles + std::uint32_t ntri = ntri_reduce.get(); + out.write(reinterpret_cast(&ntri), sizeof(std::uint32_t)); + } + else + { + out << "solid triangles\n"; + } + + // For value capture. + std::ofstream *out_ptr = &out; + const bool binary = m_binary; + + // Write triangle data. + if(mesh->getDimension() == 2 && !m_binary) + { + axom::mint::for_all_faces( + mesh, + AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType* nodes, IndexType nnodes) { + // Iterate over the face like a triangle fan. + double coords[3][2]; + mesh->getNode(nodes[0], coords[0]); + const IndexType ntri = nnodes - 2; + for(IndexType ti = 0; ti < ntri; ti++) + { + mesh->getNode(nodes[ti + 1], coords[1]); + mesh->getNode(nodes[ti + 2], coords[2]); + + *out_ptr << "facet normal 0. 0. 1.\n"; + *out_ptr << " outer loop\n"; + *out_ptr << " vertex " << coords[0][0] << " " << coords[0][1] << " 0.\n"; + *out_ptr << " vertex " << coords[1][0] << " " << coords[1][1] << " 0.\n"; + *out_ptr << " vertex " << coords[2][0] << " " << coords[2][1] << " 0.\n"; + *out_ptr << " endloop\n"; + *out_ptr << "endfacet\n"; + } + }); + } + else + { + using VectorType = axom::primal::Vector; + using float32 = float; + using uint16 = unsigned short; + + axom::mint::for_all_faces( + mesh, + AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType* nodes, IndexType nnodes) { + // Iterate over the face like a triangle fan. + double coords[3][3] = {{0., 0., 0.}, {0., 0., 0.}, {0., 0., 0.}}; + mesh->getNode(nodes[0], coords[0]); + const IndexType ntri = nnodes - 2; + for(IndexType ti = 0; ti < ntri; ti++) + { + mesh->getNode(nodes[ti + 1], coords[1]); + mesh->getNode(nodes[ti + 2], coords[2]); + + // Compute facet normal. + const VectorType A(coords[0], 3); + const VectorType B(coords[1], 3); + const VectorType C(coords[2], 3); + const VectorType N = VectorType::cross_product(B - A, C - A); + + if(binary) + { + float32 n32[3], coords32[3][3]; + for(int comp = 0; comp < 3; comp++) + { + n32[comp] = static_cast(N[comp]); + coords32[0][comp] = static_cast(coords32[0][comp]); + coords32[1][comp] = static_cast(coords32[1][comp]); + coords32[2][comp] = static_cast(coords32[2][comp]); + } + const uint16 attr = 0x7fff; + out_ptr->write(reinterpret_cast(n32), 3 * sizeof(float32)); + out_ptr->write(reinterpret_cast(coords32[0]), 3 * sizeof(float32)); + out_ptr->write(reinterpret_cast(coords32[1]), 3 * sizeof(float32)); + out_ptr->write(reinterpret_cast(coords32[2]), 3 * sizeof(float32)); + out_ptr->write(reinterpret_cast(&attr), sizeof(uint16)); + } + else + { + *out_ptr << "facet normal " << N[0] << " " << N[1] << " " << N[2] << "\n"; + *out_ptr << " outer loop\n"; + *out_ptr << " vertex " << coords[0][0] << " " << coords[0][1] << " " << coords[0][2] << "\n"; + *out_ptr << " vertex " << coords[1][0] << " " << coords[1][1] << " " << coords[1][2] << "\n"; + *out_ptr << " vertex " << coords[2][0] << " " << coords[2][1] << " " << coords[2][2] << "\n"; + *out_ptr << " endloop\n"; + *out_ptr << "endfacet\n"; + } + } + }); + } + + if(!m_binary) + { + out << "endsolid triangles\n"; + } + out.close(); + + return 0; +} + +} // namespace quest +} // namespace axom diff --git a/src/axom/quest/io/STLWriter.hpp b/src/axom/quest/io/STLWriter.hpp new file mode 100644 index 0000000000..fa9d64ca83 --- /dev/null +++ b/src/axom/quest/io/STLWriter.hpp @@ -0,0 +1,78 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef QUEST_STLWRITER_HPP_ +#define QUEST_STLWRITER_HPP_ + +// Axom includes +#include "axom/config.hpp" +#include "axom/core/Macros.hpp" +#include "axom/mint/mesh/Mesh.hpp" + +#include + +namespace axom +{ +namespace quest +{ +/*! + * \class STLWriter + * + * \brief A simple STL writer for Mint meshes. + * + * STL (STereoLithography) is a common file format for triangle meshes. + * It encodes a "soup of triangles" by explicitly listing the coordinate + * positions of the three vertices of each triangle. + */ +class STLWriter +{ +public: + /*! + * \brief Constructor. + */ + STLWriter() = default; + + /*! + * \brief Constructor. + * + * \param filename The name of the file to write. + * \param binary Whether or not to write a binary STL file. + */ + STLWriter(const std::string &filename, bool binary = false); + + /*! + * \brief Destructor. + */ + ~STLWriter() = default; + + /*! + * \brief Sets the name of the file to write. + * \param [in] fileName the name of the file to write. + */ + void setFileName(const std::string& fileName) { m_fileName = fileName; } + + /*! + * \brief Sets whether to use binary output. + * \param [in] binary True to write binary output; false to write ASCII. + */ + void setBinary(bool binary) { m_binary = binary; } + + /*! + * \brief Writes the mesh into an STL file. + * \param [in] mesh pointer to the mesh to write. + * \pre path to input file has been set by calling `setFileName()` + * \return status set to zero on success; set to a non-zero value otherwise. + */ + int write(const mint::Mesh* mesh); + +protected: + std::string m_fileName{"output.stl"}; + bool m_binary{false}; +}; + +} // namespace quest +} // namespace axom + +#endif // QUEST_STLWRITER_HPP_ From 90f39a0e3922b6be80674dad85712bd75d0b1c42 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 14:19:52 -0700 Subject: [PATCH 04/15] Tabs, add function. --- src/axom/quest/io/STLWriter.cpp | 34 +++++++++++++++++++-------------- src/axom/quest/io/STLWriter.hpp | 13 ++++++++++++- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp index 84f5b7865a..9d5ee9974a 100644 --- a/src/axom/quest/io/STLWriter.cpp +++ b/src/axom/quest/io/STLWriter.cpp @@ -76,13 +76,13 @@ int STLWriter::write(const mint::Mesh* mesh) mesh->getNode(nodes[ti + 1], coords[1]); mesh->getNode(nodes[ti + 2], coords[2]); - *out_ptr << "facet normal 0. 0. 1.\n"; - *out_ptr << " outer loop\n"; - *out_ptr << " vertex " << coords[0][0] << " " << coords[0][1] << " 0.\n"; - *out_ptr << " vertex " << coords[1][0] << " " << coords[1][1] << " 0.\n"; - *out_ptr << " vertex " << coords[2][0] << " " << coords[2][1] << " 0.\n"; - *out_ptr << " endloop\n"; - *out_ptr << "endfacet\n"; + *out_ptr << "\t facet normal 0. 0. 1.\n"; + *out_ptr << "\t\t outer loop\n"; + *out_ptr << "\t\t\t vertex " << coords[0][0] << " " << coords[0][1] << " 0.\n"; + *out_ptr << "\t\t\t vertex " << coords[1][0] << " " << coords[1][1] << " 0.\n"; + *out_ptr << "\t\t\t vertex " << coords[2][0] << " " << coords[2][1] << " 0.\n"; + *out_ptr << "\t\t endloop\n"; + *out_ptr << "\t endfacet\n"; } }); } @@ -129,13 +129,13 @@ int STLWriter::write(const mint::Mesh* mesh) } else { - *out_ptr << "facet normal " << N[0] << " " << N[1] << " " << N[2] << "\n"; - *out_ptr << " outer loop\n"; - *out_ptr << " vertex " << coords[0][0] << " " << coords[0][1] << " " << coords[0][2] << "\n"; - *out_ptr << " vertex " << coords[1][0] << " " << coords[1][1] << " " << coords[1][2] << "\n"; - *out_ptr << " vertex " << coords[2][0] << " " << coords[2][1] << " " << coords[2][2] << "\n"; - *out_ptr << " endloop\n"; - *out_ptr << "endfacet\n"; + *out_ptr << "\t facet normal " << N[0] << " " << N[1] << " " << N[2] << "\n"; + *out_ptr << "\t\t outer loop\n"; + *out_ptr << "\t\t\t vertex " << coords[0][0] << " " << coords[0][1] << " " << coords[0][2] << "\n"; + *out_ptr << "\t\t\t vertex " << coords[1][0] << " " << coords[1][1] << " " << coords[1][2] << "\n"; + *out_ptr << "\t\t\t vertex " << coords[2][0] << " " << coords[2][1] << " " << coords[2][2] << "\n"; + *out_ptr << "\t\t endloop\n"; + *out_ptr << "\t endfacet\n"; } } }); @@ -150,5 +150,11 @@ int STLWriter::write(const mint::Mesh* mesh) return 0; } +int write_stl(const mint::Mesh* mesh, const std::string &filename, bool binary) +{ + STLWriter w(filename, binary); + return w.write(mesh); +} + } // namespace quest } // namespace axom diff --git a/src/axom/quest/io/STLWriter.hpp b/src/axom/quest/io/STLWriter.hpp index fa9d64ca83..95a4137f38 100644 --- a/src/axom/quest/io/STLWriter.hpp +++ b/src/axom/quest/io/STLWriter.hpp @@ -20,7 +20,7 @@ namespace quest /*! * \class STLWriter * - * \brief A simple STL writer for Mint meshes. + * \brief A simple STL writer for the faces of Mint meshes. * * STL (STereoLithography) is a common file format for triangle meshes. * It encodes a "soup of triangles" by explicitly listing the coordinate @@ -72,6 +72,17 @@ class STLWriter bool m_binary{false}; }; +/*! + * \brief Function interface for STL writer. + * + * \param mesh The mesh whose faces will be written. + * \param filename The name of the file to write. + * \param binary Whether to write binary or ascii files. + * + * \return 0 on success; non-zero otherwise. + */ +int write_stl(const mint::Mesh* mesh, const std::string &filename, bool binary); + } // namespace quest } // namespace axom From 16e169796ff60bc1d2cec0e3cdf3dc1d52fffe8f Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 17:20:55 -0700 Subject: [PATCH 05/15] Fixes for STL writer. --- src/axom/quest/io/STLWriter.cpp | 180 ++++++++++------ src/axom/quest/io/STLWriter.hpp | 11 +- src/axom/quest/tests/CMakeLists.txt | 1 + src/axom/quest/tests/quest_stl_writer.cpp | 246 ++++++++++++++++++++++ 4 files changed, 368 insertions(+), 70 deletions(-) create mode 100644 src/axom/quest/tests/quest_stl_writer.cpp diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp index 9d5ee9974a..2385080646 100644 --- a/src/axom/quest/io/STLWriter.cpp +++ b/src/axom/quest/io/STLWriter.cpp @@ -15,18 +15,93 @@ namespace axom { namespace quest { +namespace internal +{ + +/*! + * \brief Write a triangle to an output stream. + * + * \param out The stream to use for writing. + * \param binary Whether to write the triangle in binary format. The file must + * have been opened in binary mode. + * \param coords The triangle coordinates. + * \param N The triangle normal. + */ +template +void writeTriangle(std::ofstream &out, bool binary, double coords[3][3], const NormalType &N) +{ + if(binary) + { + using float32 = float; + using uint16 = unsigned short; + float32 n32[3], coords32[3][3]; + for(int comp = 0; comp < 3; comp++) + { + n32[comp] = static_cast(N[comp]); + coords32[0][comp] = static_cast(coords[0][comp]); + coords32[1][comp] = static_cast(coords[1][comp]); + coords32[2][comp] = static_cast(coords[2][comp]); + } + const uint16 attr = 0x7fff; + out.write(reinterpret_cast(n32), 3 * sizeof(float32)); + out.write(reinterpret_cast(coords32[0]), 3 * sizeof(float32)); + out.write(reinterpret_cast(coords32[1]), 3 * sizeof(float32)); + out.write(reinterpret_cast(coords32[2]), 3 * sizeof(float32)); + out.write(reinterpret_cast(&attr), sizeof(uint16)); + } + else + { + out << "\t facet normal " << N[0] << " " << N[1] << " " << N[2] << "\n"; + out << "\t\t outer loop\n"; + out << "\t\t\t vertex " << coords[0][0] << " " << coords[0][1] << " " << coords[0][2] << "\n"; + out << "\t\t\t vertex " << coords[1][0] << " " << coords[1][1] << " " << coords[1][2] << "\n"; + out << "\t\t\t vertex " << coords[2][0] << " " << coords[2][1] << " " << coords[2][2] << "\n"; + out << "\t\t endloop\n"; + out << "\t endfacet\n"; + } +} + +} // end namespace internal STLWriter::STLWriter(const std::string &filename, bool binary) : m_fileName(filename), m_binary(binary) { } +IndexType STLWriter::getNumberOfTriangles(const mint::Mesh* mesh) const +{ + IndexType ntri = 0; + if(mesh->getDimension() == 2) + { + for(IndexType cellId = 0; cellId < mesh->getNumberOfCells(); cellId++) + { + ntri += (mesh->getNumberOfCellNodes(cellId) - 2); + } + } + else if(mesh->getDimension() == 3) + { + axom::ReduceSum ntri_reduce(0); + axom::mint::for_all_faces( + mesh, + AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType* AXOM_UNUSED_PARAM(nodes), IndexType N) { + ntri_reduce += (N - 2); + }); + ntri = ntri_reduce.get(); + } + return ntri; +} + int STLWriter::write(const mint::Mesh* mesh) { SLIC_ERROR_IF(mesh == nullptr, "mesh pointer is null!"); SLIC_ERROR_IF(m_fileName.length() <= 0, "STL filename is empty!"); - SLIC_ERROR_IF(mesh->getDimension() == 2 || mesh->getDimension() == 3, "Input mesh must is not 2D/3D."); + SLIC_ERROR_IF(mesh->getDimension() < 2 || mesh->getDimension() > 3, "Input mesh must is not 2D/3D."); - std::ofstream out(m_fileName.c_str(), m_binary ? std::ofstream::binary : std::ofstream::out); + auto flags = std::ofstream::out; + if(m_binary) + { + flags |= std::ofstream::binary; + } + std::ofstream out(m_fileName.c_str(), flags); if(!out.is_open()) { SLIC_WARNING("cannot write to file [" << m_fileName << "]"); @@ -36,20 +111,17 @@ int STLWriter::write(const mint::Mesh* mesh) // Write header if(m_binary) { - std::uint8_t header[80] = {}; - strcpy(reinterpret_cast(header), "STL Binary Header Written by Axom"); - out.write(reinterpret_cast(header), 80); - - // Compute the number of triangles we'll write. - axom::ReduceSum ntri_reduce(0); - axom::mint::for_all_faces( - mesh, - AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType* AXOM_UNUSED_PARAM(nodes), IndexType N) { - ntri_reduce += static_cast(N - 2); - }); + constexpr int STL_HEADER_SIZE = 80; + std::uint8_t header[STL_HEADER_SIZE]; + // Fill with spaces + memset(header, ' ', sizeof(std::uint8_t) * STL_HEADER_SIZE); + // Copy in string (without terminator) + const char *msg = "STL Binary File Written By Axom"; + memcpy(header, msg, strlen(msg)); + out.write(reinterpret_cast(header), STL_HEADER_SIZE); // Write number of triangles - std::uint32_t ntri = ntri_reduce.get(); + std::uint32_t ntri = static_cast(getNumberOfTriangles(mesh)); out.write(reinterpret_cast(&ntri), sizeof(std::uint32_t)); } else @@ -57,40 +129,36 @@ int STLWriter::write(const mint::Mesh* mesh) out << "solid triangles\n"; } - // For value capture. - std::ofstream *out_ptr = &out; - const bool binary = m_binary; - // Write triangle data. - if(mesh->getDimension() == 2 && !m_binary) + if(mesh->getDimension() == 2) { - axom::mint::for_all_faces( - mesh, - AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType* nodes, IndexType nnodes) { - // Iterate over the face like a triangle fan. - double coords[3][2]; - mesh->getNode(nodes[0], coords[0]); - const IndexType ntri = nnodes - 2; - for(IndexType ti = 0; ti < ntri; ti++) - { - mesh->getNode(nodes[ti + 1], coords[1]); - mesh->getNode(nodes[ti + 2], coords[2]); - - *out_ptr << "\t facet normal 0. 0. 1.\n"; - *out_ptr << "\t\t outer loop\n"; - *out_ptr << "\t\t\t vertex " << coords[0][0] << " " << coords[0][1] << " 0.\n"; - *out_ptr << "\t\t\t vertex " << coords[1][0] << " " << coords[1][1] << " 0.\n"; - *out_ptr << "\t\t\t vertex " << coords[2][0] << " " << coords[2][1] << " 0.\n"; - *out_ptr << "\t\t endloop\n"; - *out_ptr << "\t endfacet\n"; - } - }); + axom::Array nodes; + axom::StackArray N{{0., 0., 1.}}; + double coords[3][3] = {{0., 0., 0.}, {0., 0., 0.}, {0., 0., 0.}}; + for(axom::IndexType cellId = 0; cellId < mesh->getNumberOfCells(); cellId++) + { + // Get nodes for this cell. + nodes.resize(mesh->getNumberOfCellNodes(cellId)); + const auto nnodes = mesh->getCellNodeIDs(cellId, nodes.data()); + + // Iterate over the face like a triangle fan. + mesh->getNode(nodes[0], coords[0]); + const IndexType ntri = nnodes - 2; + for(IndexType ti = 0; ti < ntri; ti++) + { + mesh->getNode(nodes[ti + 1], coords[1]); + mesh->getNode(nodes[ti + 2], coords[2]); + internal::writeTriangle(out, m_binary, coords, N); + } + } } else { using VectorType = axom::primal::Vector; - using float32 = float; - using uint16 = unsigned short; + + // For value capture. + std::ofstream *out_ptr = &out; + const bool binary = m_binary; axom::mint::for_all_faces( mesh, @@ -110,33 +178,7 @@ int STLWriter::write(const mint::Mesh* mesh) const VectorType C(coords[2], 3); const VectorType N = VectorType::cross_product(B - A, C - A); - if(binary) - { - float32 n32[3], coords32[3][3]; - for(int comp = 0; comp < 3; comp++) - { - n32[comp] = static_cast(N[comp]); - coords32[0][comp] = static_cast(coords32[0][comp]); - coords32[1][comp] = static_cast(coords32[1][comp]); - coords32[2][comp] = static_cast(coords32[2][comp]); - } - const uint16 attr = 0x7fff; - out_ptr->write(reinterpret_cast(n32), 3 * sizeof(float32)); - out_ptr->write(reinterpret_cast(coords32[0]), 3 * sizeof(float32)); - out_ptr->write(reinterpret_cast(coords32[1]), 3 * sizeof(float32)); - out_ptr->write(reinterpret_cast(coords32[2]), 3 * sizeof(float32)); - out_ptr->write(reinterpret_cast(&attr), sizeof(uint16)); - } - else - { - *out_ptr << "\t facet normal " << N[0] << " " << N[1] << " " << N[2] << "\n"; - *out_ptr << "\t\t outer loop\n"; - *out_ptr << "\t\t\t vertex " << coords[0][0] << " " << coords[0][1] << " " << coords[0][2] << "\n"; - *out_ptr << "\t\t\t vertex " << coords[1][0] << " " << coords[1][1] << " " << coords[1][2] << "\n"; - *out_ptr << "\t\t\t vertex " << coords[2][0] << " " << coords[2][1] << " " << coords[2][2] << "\n"; - *out_ptr << "\t\t endloop\n"; - *out_ptr << "\t endfacet\n"; - } + internal::writeTriangle(*out_ptr, binary, coords, N); } }); } diff --git a/src/axom/quest/io/STLWriter.hpp b/src/axom/quest/io/STLWriter.hpp index 95a4137f38..1c7f762162 100644 --- a/src/axom/quest/io/STLWriter.hpp +++ b/src/axom/quest/io/STLWriter.hpp @@ -68,6 +68,15 @@ class STLWriter int write(const mint::Mesh* mesh); protected: + /*! + * \brief Compute the number of triangles produced for the input mesh. + * + * \param mesh The input mesh. + * + * \return The number of triangles. + */ + IndexType getNumberOfTriangles(const mint::Mesh* mesh) const; + std::string m_fileName{"output.stl"}; bool m_binary{false}; }; @@ -81,7 +90,7 @@ class STLWriter * * \return 0 on success; non-zero otherwise. */ -int write_stl(const mint::Mesh* mesh, const std::string &filename, bool binary); +int write_stl(const mint::Mesh* mesh, const std::string &filename, bool binary = false); } // namespace quest } // namespace axom diff --git a/src/axom/quest/tests/CMakeLists.txt b/src/axom/quest/tests/CMakeLists.txt index b094afd6e6..35da691a24 100644 --- a/src/axom/quest/tests/CMakeLists.txt +++ b/src/axom/quest/tests/CMakeLists.txt @@ -14,6 +14,7 @@ set(quest_tests quest_discretize.cpp quest_pro_e_reader.cpp quest_stl_reader.cpp + quest_stl_writer.cpp quest_vertex_weld.cpp ) diff --git a/src/axom/quest/tests/quest_stl_writer.cpp b/src/axom/quest/tests/quest_stl_writer.cpp new file mode 100644 index 0000000000..d8894efbde --- /dev/null +++ b/src/axom/quest/tests/quest_stl_writer.cpp @@ -0,0 +1,246 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/quest/io/STLWriter.hpp" +#include "axom/quest/io/STLReader.hpp" +#include "axom/mint/mesh/UniformMesh.hpp" +#include "axom/mint/mesh/UnstructuredMesh.hpp" +#include "axom/slic.hpp" +#include "axom/fmt.hpp" + +// gtest includes +#include "gtest/gtest.h" + +// C/C++ includes +#include +#include +#include + +// Uncomment this line to write new baseline data to stdout. +// #define AXOM_SET_BASELINES + +// namespace aliases +namespace mint = axom::mint; +namespace quest = axom::quest; + +namespace testing +{ + +void writeArray(const axom::Array& vec, const std::string& var_name = "v") +{ + axom::fmt::print("axom::Array {} = {{{}}};\n", var_name, axom::fmt::join(vec, ", ")); +} + + /// Convert mesh coordinates into arrays that can be easily compared. + void getCoordinates(const mint::Mesh &mesh, axom::Array &xc, axom::Array & yc) + { + for(axom::IndexType cellId = 0; cellId < mesh.getNumberOfCells(); cellId++) + { + EXPECT_EQ(mesh.getNumberOfCellNodes(cellId), 3); + + axom::IndexType nodes[3]; + mesh.getCellNodeIDs(cellId, nodes); + for(int i = 0; i < 3; i++) + { + xc.push_back(mesh.getCoordinateArray(mint::X_COORDINATE)[nodes[i]]); + yc.push_back(mesh.getCoordinateArray(mint::Y_COORDINATE)[nodes[i]]); + } + } + } + +bool compareArrays(const axom::Array &A, const axom::Array &B, double tolerance = 1.e-8) +{ + bool eq = A.size() == B.size(); + if(eq) + { + for(axom::IndexType i = 0; i < A.size() && eq; i++) + { + eq &= axom::utilities::isNearlyEqual(A[i], B[i], tolerance); + if(!eq) + { + SLIC_ERROR(axom::fmt::format("Difference at index {}: {}, {}", i, A[i], B[i])); + } + } + } + return eq; +} + +struct Test2D +{ + axom::Array baselineXCoordinates() + { + return axom::Array{{0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; + } + + axom::Array baselineYCoordinates() + { + return axom::Array{{1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; + } + + void test(const mint::Mesh &mesh, const std::string &filename, bool binary) + { + // Write STL file. + int result = axom::quest::write_stl(&mesh, filename, binary); + SLIC_INFO(axom::fmt::format("Writing {} -> {}", filename, (result == 0) ? "ok" : "error")); + EXPECT_EQ(result, 0); + + // Read file back into memory and check the mesh. + axom::quest::STLReader reader; + reader.setFileName(filename); + result = reader.read(); + SLIC_INFO(axom::fmt::format("Reading {} -> {}", filename, (result == 0) ? "ok" : "error")); + EXPECT_EQ(result, 0); + if(result == 0) + { +// std::cout << "reader.getNumNodes()=" << reader.getNumNodes() << std::endl; +// std::cout << "reader.getNumFaces()=" << reader.getNumFaces() << std::endl; + + // Get the file data as a mint mesh. + mint::UnstructuredMesh readMesh(3, mint::CellType::TRIANGLE); + reader.getMesh(&readMesh); + EXPECT_EQ(readMesh.getNumberOfCells(), 8); + +// std::cout << "readMesh.getNumberOfFaces()=" << readMesh.getNumberOfCells() << std::endl; + axom::Array xc, yc; + getCoordinates(readMesh, xc, yc); +#if defined(AXOM_SET_BASELINES) + // Write out results to put into source code. + testing::writeArray(xc, "xc"); + testing::writeArray(yc, "yc"); +#else + EXPECT_TRUE(testing::compareArrays(xc, baselineXCoordinates())); + EXPECT_TRUE(testing::compareArrays(yc, baselineYCoordinates())); +#endif + } + +// axom::utilities::filesystem::removeFile(filename); + } +}; + +} // end namespace testing + +//------------------------------------------------------------------------------ +// UNIT TESTS +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +TEST(quest_stl_writer, uniform2d) +{ + // Make mesh. + const double lower_bound[] = {0., 1.}; + const double upper_bound[] = {1., 2.}; + constexpr axom::IndexType NI = 3; + constexpr axom::IndexType NJ = 3; + mint::UniformMesh mesh(lower_bound, upper_bound, NI, NJ); + + testing::Test2D tester; + tester.test(mesh, "uniform2d.stl", false); + tester.test(mesh, "uniform2dB.stl", true); +} + +#if 0 + const double x_expected[] = {0.0, 1.0, 0.0}; + const double y_expected[] = {0.0, 0.0, 1.0}; + const double z_expected[] = {0.0, 0.0, 0.0}; + + const std::string filename = "triangle.stl"; + + // STEP 0: generate a temporary STL file for testing + generate_stl_file(filename); + + // STEP 1: create an STL reader and read-in the mesh data + quest::STLReader reader; + reader.setFileName(filename); + int status = reader.read(); + EXPECT_EQ(status, 0); + + // STEP 2: reading the STL mesh data into a mint::Mesh + mint::UnstructuredMesh mesh(3, mint::TRIANGLE); + reader.getMesh(&mesh); + + // STEP 3: ensure the mesh is what is expected + EXPECT_EQ(mesh.getNumberOfCells(), 1); + EXPECT_EQ(mesh.getNumberOfNodes(), 3); + + const double* x = mesh.getCoordinateArray(mint::X_COORDINATE); + const double* y = mesh.getCoordinateArray(mint::Y_COORDINATE); + const double* z = mesh.getCoordinateArray(mint::Z_COORDINATE); + EXPECT_TRUE(x != nullptr); + EXPECT_TRUE(y != nullptr); + EXPECT_TRUE(z != nullptr); + + axom::IndexType numNodes = mesh.getNumberOfNodes(); + for(axom::IndexType inode = 0; inode < numNodes; ++inode) + { + EXPECT_NEAR(x[inode], x_expected[inode], axom::numeric_limits::epsilon()); + EXPECT_NEAR(y[inode], y_expected[inode], axom::numeric_limits::epsilon()); + EXPECT_NEAR(z[inode], z_expected[inode], axom::numeric_limits::epsilon()); + } // END for all nodes + + // STEP 4: remove temporary STL file + axom::utilities::filesystem::removeFile(filename); +} + +//------------------------------------------------------------------------------ +TEST(quest_stl_writer, read_stl_external) +{ + constexpr axom::IndexType N_NODES = 3; + constexpr axom::IndexType N_FACES = 1; + const double x_expected[] = {0.0, 1.0, 0.0}; + const double y_expected[] = {0.0, 0.0, 1.0}; + const double z_expected[] = {0.0, 0.0, 0.0}; + + double xin[] = {-1.0, -1.0, -1.0}; + double yin[] = {-1.0, -1.0, -1.0}; + double zin[] = {-1.0, -1.0, -1.0}; + + axom::IndexType conn[] = {-1, -1, -1}; + + const std::string filename = "triangle.stl"; + + // STEP 0: generate a temporary STL file for testing + generate_stl_file(filename); + + // STEP 1: create an STL reader and read-in the mesh data + quest::STLReader reader; + reader.setFileName(filename); + int status = reader.read(); + EXPECT_EQ(status, 0); + + // STEP 2: reading the STL mesh data into a mint::Mesh + mint::UnstructuredMesh mesh(mint::TRIANGLE, N_FACES, conn, N_NODES, xin, yin, zin); + EXPECT_EQ(mesh.getNumberOfCells(), N_FACES); + EXPECT_EQ(mesh.getNumberOfNodes(), N_NODES); + + reader.getMesh(&mesh); + + const double* x = mesh.getCoordinateArray(mint::X_COORDINATE); + const double* y = mesh.getCoordinateArray(mint::Y_COORDINATE); + const double* z = mesh.getCoordinateArray(mint::Z_COORDINATE); + EXPECT_TRUE(x != nullptr); + EXPECT_TRUE(y != nullptr); + EXPECT_TRUE(z != nullptr); + + axom::IndexType numNodes = mesh.getNumberOfNodes(); + for(axom::IndexType inode = 0; inode < numNodes; ++inode) + { + EXPECT_NEAR(x[inode], x_expected[inode], axom::numeric_limits::epsilon()); + EXPECT_NEAR(y[inode], y_expected[inode], axom::numeric_limits::epsilon()); + EXPECT_NEAR(z[inode], z_expected[inode], axom::numeric_limits::epsilon()); + } // END for all nodes + + // STEP 4: remove temporary STL file + axom::utilities::filesystem::removeFile(filename); +} +#endif + +//------------------------------------------------------------------------------ +int main(int argc, char* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + axom::slic::SimpleLogger logger; + + return RUN_ALL_TESTS(); +} From e5607e7b9a948bbe4f820c76cadb4025c5a0762a Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 17:31:06 -0700 Subject: [PATCH 06/15] rectilinear 2d --- src/axom/quest/io/STLWriter.hpp | 4 ++++ src/axom/quest/tests/quest_stl_writer.cpp | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/axom/quest/io/STLWriter.hpp b/src/axom/quest/io/STLWriter.hpp index 1c7f762162..4dfb5926dc 100644 --- a/src/axom/quest/io/STLWriter.hpp +++ b/src/axom/quest/io/STLWriter.hpp @@ -25,6 +25,10 @@ namespace quest * STL (STereoLithography) is a common file format for triangle meshes. * It encodes a "soup of triangles" by explicitly listing the coordinate * positions of the three vertices of each triangle. + * + * \note Any faces that contain more than 3 vertices will be treated + * like a triangle fan and will result in multiple triangles. + * No care is taken to prevent triangle overlaps for non-convex faces. */ class STLWriter { diff --git a/src/axom/quest/tests/quest_stl_writer.cpp b/src/axom/quest/tests/quest_stl_writer.cpp index d8894efbde..dc892952dd 100644 --- a/src/axom/quest/tests/quest_stl_writer.cpp +++ b/src/axom/quest/tests/quest_stl_writer.cpp @@ -7,6 +7,7 @@ #include "axom/quest/io/STLReader.hpp" #include "axom/mint/mesh/UniformMesh.hpp" #include "axom/mint/mesh/UnstructuredMesh.hpp" +#include "axom/mint/mesh/RectilinearMesh.hpp" #include "axom/slic.hpp" #include "axom/fmt.hpp" @@ -140,6 +141,20 @@ TEST(quest_stl_writer, uniform2d) tester.test(mesh, "uniform2dB.stl", true); } +TEST(quest_stl_writer, rectilinear2d) +{ + // Make mesh. + const double x[] = {0., 0.5, 1.}; + const double y[] = {1., 1.5, 2.}; + constexpr axom::IndexType NI = 3; + constexpr axom::IndexType NJ = 3; + mint::RectilinearMesh mesh(NI, const_cast(x), NJ, const_cast(y)); + + testing::Test2D tester; + tester.test(mesh, "rectilinear2d.stl", false); + tester.test(mesh, "rectilinear2dB.stl", true); +} + #if 0 const double x_expected[] = {0.0, 1.0, 0.0}; const double y_expected[] = {0.0, 0.0, 1.0}; From 1c8a959ff3302df5d059048f1ea1f154fbf9db1e Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 17:57:43 -0700 Subject: [PATCH 07/15] 2D STLWriter cases work. --- src/axom/quest/io/STLWriter.cpp | 7 +- src/axom/quest/tests/quest_stl_writer.cpp | 122 ++++++---------------- 2 files changed, 33 insertions(+), 96 deletions(-) diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp index 2385080646..5b9f4d9e49 100644 --- a/src/axom/quest/io/STLWriter.cpp +++ b/src/axom/quest/io/STLWriter.cpp @@ -96,12 +96,7 @@ int STLWriter::write(const mint::Mesh* mesh) SLIC_ERROR_IF(m_fileName.length() <= 0, "STL filename is empty!"); SLIC_ERROR_IF(mesh->getDimension() < 2 || mesh->getDimension() > 3, "Input mesh must is not 2D/3D."); - auto flags = std::ofstream::out; - if(m_binary) - { - flags |= std::ofstream::binary; - } - std::ofstream out(m_fileName.c_str(), flags); + std::ofstream out(m_fileName.c_str(), m_binary ? (std::ofstream::out | std::ofstream::binary) : std::ofstream::out); if(!out.is_open()) { SLIC_WARNING("cannot write to file [" << m_fileName << "]"); diff --git a/src/axom/quest/tests/quest_stl_writer.cpp b/src/axom/quest/tests/quest_stl_writer.cpp index dc892952dd..662926e5bd 100644 --- a/src/axom/quest/tests/quest_stl_writer.cpp +++ b/src/axom/quest/tests/quest_stl_writer.cpp @@ -8,6 +8,7 @@ #include "axom/mint/mesh/UniformMesh.hpp" #include "axom/mint/mesh/UnstructuredMesh.hpp" #include "axom/mint/mesh/RectilinearMesh.hpp" +#include "axom/mint/mesh/CurvilinearMesh.hpp" #include "axom/slic.hpp" #include "axom/fmt.hpp" @@ -126,7 +127,6 @@ struct Test2D // UNIT TESTS //------------------------------------------------------------------------------ -//------------------------------------------------------------------------------ TEST(quest_stl_writer, uniform2d) { // Make mesh. @@ -155,101 +155,43 @@ TEST(quest_stl_writer, rectilinear2d) tester.test(mesh, "rectilinear2dB.stl", true); } -#if 0 - const double x_expected[] = {0.0, 1.0, 0.0}; - const double y_expected[] = {0.0, 0.0, 1.0}; - const double z_expected[] = {0.0, 0.0, 0.0}; - - const std::string filename = "triangle.stl"; - - // STEP 0: generate a temporary STL file for testing - generate_stl_file(filename); - - // STEP 1: create an STL reader and read-in the mesh data - quest::STLReader reader; - reader.setFileName(filename); - int status = reader.read(); - EXPECT_EQ(status, 0); - - // STEP 2: reading the STL mesh data into a mint::Mesh - mint::UnstructuredMesh mesh(3, mint::TRIANGLE); - reader.getMesh(&mesh); - - // STEP 3: ensure the mesh is what is expected - EXPECT_EQ(mesh.getNumberOfCells(), 1); - EXPECT_EQ(mesh.getNumberOfNodes(), 3); - - const double* x = mesh.getCoordinateArray(mint::X_COORDINATE); - const double* y = mesh.getCoordinateArray(mint::Y_COORDINATE); - const double* z = mesh.getCoordinateArray(mint::Z_COORDINATE); - EXPECT_TRUE(x != nullptr); - EXPECT_TRUE(y != nullptr); - EXPECT_TRUE(z != nullptr); - - axom::IndexType numNodes = mesh.getNumberOfNodes(); - for(axom::IndexType inode = 0; inode < numNodes; ++inode) - { - EXPECT_NEAR(x[inode], x_expected[inode], axom::numeric_limits::epsilon()); - EXPECT_NEAR(y[inode], y_expected[inode], axom::numeric_limits::epsilon()); - EXPECT_NEAR(z[inode], z_expected[inode], axom::numeric_limits::epsilon()); - } // END for all nodes +TEST(quest_stl_writer, curvilinear2d) +{ + // Make mesh. + const double x[] = {0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1.}; + const double y[] = {1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2.}; + constexpr axom::IndexType NI = 3; + constexpr axom::IndexType NJ = 3; + mint::CurvilinearMesh mesh(NI, const_cast(x), NJ, const_cast(y)); - // STEP 4: remove temporary STL file - axom::utilities::filesystem::removeFile(filename); + testing::Test2D tester; + tester.test(mesh, "curvilinear2d.stl", false); + tester.test(mesh, "curvilinear2dB.stl", true); } -//------------------------------------------------------------------------------ -TEST(quest_stl_writer, read_stl_external) +TEST(quest_stl_writer, unstructured2d) { - constexpr axom::IndexType N_NODES = 3; - constexpr axom::IndexType N_FACES = 1; - const double x_expected[] = {0.0, 1.0, 0.0}; - const double y_expected[] = {0.0, 0.0, 1.0}; - const double z_expected[] = {0.0, 0.0, 0.0}; - - double xin[] = {-1.0, -1.0, -1.0}; - double yin[] = {-1.0, -1.0, -1.0}; - double zin[] = {-1.0, -1.0, -1.0}; - - axom::IndexType conn[] = {-1, -1, -1}; - - const std::string filename = "triangle.stl"; - - // STEP 0: generate a temporary STL file for testing - generate_stl_file(filename); - - // STEP 1: create an STL reader and read-in the mesh data - quest::STLReader reader; - reader.setFileName(filename); - int status = reader.read(); - EXPECT_EQ(status, 0); - - // STEP 2: reading the STL mesh data into a mint::Mesh - mint::UnstructuredMesh mesh(mint::TRIANGLE, N_FACES, conn, N_NODES, xin, yin, zin); - EXPECT_EQ(mesh.getNumberOfCells(), N_FACES); - EXPECT_EQ(mesh.getNumberOfNodes(), N_NODES); - - reader.getMesh(&mesh); - - const double* x = mesh.getCoordinateArray(mint::X_COORDINATE); - const double* y = mesh.getCoordinateArray(mint::Y_COORDINATE); - const double* z = mesh.getCoordinateArray(mint::Z_COORDINATE); - EXPECT_TRUE(x != nullptr); - EXPECT_TRUE(y != nullptr); - EXPECT_TRUE(z != nullptr); - - axom::IndexType numNodes = mesh.getNumberOfNodes(); - for(axom::IndexType inode = 0; inode < numNodes; ++inode) - { - EXPECT_NEAR(x[inode], x_expected[inode], axom::numeric_limits::epsilon()); - EXPECT_NEAR(y[inode], y_expected[inode], axom::numeric_limits::epsilon()); - EXPECT_NEAR(z[inode], z_expected[inode], axom::numeric_limits::epsilon()); - } // END for all nodes + // Make mesh. + const double x[] = {0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1.}; + const double y[] = {1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2.}; + const axom::IndexType conn[] = { + 0, 1, 4, 0, 4, 3, 1, 2, 5, 1, 5, 4, 3, 4, 7, 3, 7, 6, 4, 5, 8, 4, 8, 7 + }; + constexpr axom::IndexType nnodes = 9; + constexpr axom::IndexType numTriangles = 8; + mint::UnstructuredMesh mesh(mint::CellType::TRIANGLE, + numTriangles, // ncells + numTriangles, // cell_capacity + const_cast(conn), + nnodes, // nnodes + nnodes, // node_capacity + const_cast(x), + const_cast(y)); - // STEP 4: remove temporary STL file - axom::utilities::filesystem::removeFile(filename); + testing::Test2D tester; + tester.test(mesh, "unstructured2d.stl", false); + tester.test(mesh, "unstructured2dB.stl", true); } -#endif //------------------------------------------------------------------------------ int main(int argc, char* argv[]) From 2e6ff8e25f3cc073bce4c8c68b92b3674cdff3dd Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 18:44:08 -0700 Subject: [PATCH 08/15] 3D tests --- src/axom/quest/tests/quest_stl_writer.cpp | 214 ++++++++++++++++++++-- 1 file changed, 198 insertions(+), 16 deletions(-) diff --git a/src/axom/quest/tests/quest_stl_writer.cpp b/src/axom/quest/tests/quest_stl_writer.cpp index 662926e5bd..0126939d91 100644 --- a/src/axom/quest/tests/quest_stl_writer.cpp +++ b/src/axom/quest/tests/quest_stl_writer.cpp @@ -35,22 +35,43 @@ void writeArray(const axom::Array& vec, const std::string& var_name = "v axom::fmt::print("axom::Array {} = {{{}}};\n", var_name, axom::fmt::join(vec, ", ")); } - /// Convert mesh coordinates into arrays that can be easily compared. - void getCoordinates(const mint::Mesh &mesh, axom::Array &xc, axom::Array & yc) +/// Convert mesh coordinates into arrays that can be easily compared. +void getCoordinates(const mint::Mesh &mesh, axom::Array &xc, axom::Array & yc) +{ + for(axom::IndexType cellId = 0; cellId < mesh.getNumberOfCells(); cellId++) { - for(axom::IndexType cellId = 0; cellId < mesh.getNumberOfCells(); cellId++) + EXPECT_EQ(mesh.getNumberOfCellNodes(cellId), 3); + + axom::IndexType nodes[3]; + mesh.getCellNodeIDs(cellId, nodes); + for(int i = 0; i < 3; i++) { - EXPECT_EQ(mesh.getNumberOfCellNodes(cellId), 3); + xc.push_back(mesh.getCoordinateArray(mint::X_COORDINATE)[nodes[i]]); + yc.push_back(mesh.getCoordinateArray(mint::Y_COORDINATE)[nodes[i]]); + } + } +} - axom::IndexType nodes[3]; - mesh.getCellNodeIDs(cellId, nodes); - for(int i = 0; i < 3; i++) - { - xc.push_back(mesh.getCoordinateArray(mint::X_COORDINATE)[nodes[i]]); - yc.push_back(mesh.getCoordinateArray(mint::Y_COORDINATE)[nodes[i]]); - } +/// Convert mesh coordinates into arrays that can be easily compared. +void getCoordinates(const mint::Mesh &mesh, + axom::Array &xc, + axom::Array &yc, + axom::Array &zc) +{ + for(axom::IndexType cellId = 0; cellId < mesh.getNumberOfCells(); cellId++) + { + EXPECT_EQ(mesh.getNumberOfCellNodes(cellId), 3); + + axom::IndexType nodes[3]; + mesh.getCellNodeIDs(cellId, nodes); + for(int i = 0; i < 3; i++) + { + xc.push_back(mesh.getCoordinateArray(mint::X_COORDINATE)[nodes[i]]); + yc.push_back(mesh.getCoordinateArray(mint::Y_COORDINATE)[nodes[i]]); + zc.push_back(mesh.getCoordinateArray(mint::Z_COORDINATE)[nodes[i]]); } } +} bool compareArrays(const axom::Array &A, const axom::Array &B, double tolerance = 1.e-8) { @@ -69,6 +90,10 @@ bool compareArrays(const axom::Array &A, const axom::Array &B, d return eq; } +/*! + * \brief This testing class writes a mesh to STL, reads it back, and checks + * that the coordinates used by all the triangles are as-expected. + */ struct Test2D { axom::Array baselineXCoordinates() @@ -96,15 +121,11 @@ struct Test2D EXPECT_EQ(result, 0); if(result == 0) { -// std::cout << "reader.getNumNodes()=" << reader.getNumNodes() << std::endl; -// std::cout << "reader.getNumFaces()=" << reader.getNumFaces() << std::endl; - // Get the file data as a mint mesh. mint::UnstructuredMesh readMesh(3, mint::CellType::TRIANGLE); reader.getMesh(&readMesh); EXPECT_EQ(readMesh.getNumberOfCells(), 8); -// std::cout << "readMesh.getNumberOfFaces()=" << readMesh.getNumberOfCells() << std::endl; axom::Array xc, yc; getCoordinates(readMesh, xc, yc); #if defined(AXOM_SET_BASELINES) @@ -117,16 +138,96 @@ struct Test2D #endif } + axom::utilities::filesystem::removeFile(filename); + } +}; + +/*! + * \brief This testing class writes a mesh to STL, reads it back, and checks + * that the coordinates used by all the triangles are as-expected. + */ +struct Test3D +{ + virtual axom::Array baselineXCoordinates() + { + return axom::Array{{0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; + } + + virtual axom::Array baselineYCoordinates() + { + return axom::Array{{1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; + } + + virtual axom::Array baselineZCoordinates() + { + return axom::Array{{2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}}; + } + + void test(const mint::Mesh &mesh, const std::string &filename, bool binary) + { + // Write STL file. + int result = axom::quest::write_stl(&mesh, filename, binary); + SLIC_INFO(axom::fmt::format("Writing {} -> {}", filename, (result == 0) ? "ok" : "error")); + EXPECT_EQ(result, 0); + + // Read file back into memory and check the mesh. + axom::quest::STLReader reader; + reader.setFileName(filename); + result = reader.read(); + SLIC_INFO(axom::fmt::format("Reading {} -> {}", filename, (result == 0) ? "ok" : "error")); + EXPECT_EQ(result, 0); + if(result == 0) + { + // Get the file data as a mint mesh. + mint::UnstructuredMesh readMesh(3, mint::CellType::TRIANGLE); + reader.getMesh(&readMesh); + EXPECT_EQ(readMesh.getNumberOfCells(), 40); + + axom::Array xc, yc, zc; + getCoordinates(readMesh, xc, yc, zc); +#if defined(AXOM_SET_BASELINES) + // Write out results to put into source code. + testing::writeArray(xc, "xc"); + testing::writeArray(yc, "yc"); + testing::writeArray(zc, "zc"); +#else + EXPECT_TRUE(testing::compareArrays(xc, baselineXCoordinates())); + EXPECT_TRUE(testing::compareArrays(yc, baselineYCoordinates())); + EXPECT_TRUE(testing::compareArrays(zc, baselineZCoordinates())); +#endif + } + // axom::utilities::filesystem::removeFile(filename); } }; +/*! + * \brief This testing class writes a mesh to STL, reads it back, and checks + * that the coordinates used by all the triangles are as-expected. + */ +struct Test3DUns : public Test3D +{ + virtual axom::Array baselineXCoordinates() override + { + return axom::Array{{0, 0, 0.5, 0, 0.5, 0.5, 0.5, 0.5, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 1, 0.5, 1, 1, 1, 1, 0.5, 1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0, 0.5, 0, 0.5, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 1, 0.5, 1, 1, 0.5, 1, 1, 0.5, 1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; + } + + virtual axom::Array baselineYCoordinates() override + { + return axom::Array{{1, 1.5, 1.5, 1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1.5, 1.5, 1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1.5, 1, 1.5, 1, 1, 1.5, 1.5, 1, 1.5, 1, 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 2, 2, 1.5, 2, 1.5, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; + } + + virtual axom::Array baselineZCoordinates() override + { + return axom::Array{{2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}}; + } +}; + } // end namespace testing //------------------------------------------------------------------------------ // UNIT TESTS //------------------------------------------------------------------------------ - TEST(quest_stl_writer, uniform2d) { // Make mesh. @@ -193,6 +294,87 @@ TEST(quest_stl_writer, unstructured2d) tester.test(mesh, "unstructured2dB.stl", true); } +TEST(quest_stl_writer, uniform3d) +{ + // Make mesh. + const double lower_bound[] = {0., 1., 2.}; + const double upper_bound[] = {1., 2., 3.}; + constexpr axom::IndexType NI = 3; + constexpr axom::IndexType NJ = 3; + constexpr axom::IndexType NK = 2; + mint::UniformMesh mesh(lower_bound, upper_bound, NI, NJ, NK); + + testing::Test3D tester; + tester.test(mesh, "uniform3d.stl", false); + tester.test(mesh, "uniform3dB.stl", true); +} + +TEST(quest_stl_writer, rectilinear3d) +{ + // Make mesh. + const double x[] = {0., 0.5, 1.}; + const double y[] = {1., 1.5, 2.}; + const double z[] = {2., 3.}; + constexpr axom::IndexType NI = 3; + constexpr axom::IndexType NJ = 3; + constexpr axom::IndexType NK = 2; + mint::RectilinearMesh mesh(NI, const_cast(x), + NJ, const_cast(y), + NK, const_cast(z)); + + testing::Test3D tester; + tester.test(mesh, "rectilinear3d.stl", false); + tester.test(mesh, "rectilinear3dB.stl", true); +} + +TEST(quest_stl_writer, curvilinear3d) +{ + // Make mesh. + const double x[] = {0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1.}; + const double y[] = {1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2., 1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2.}; + const double z[] = {2., 2., 2., 2., 2., 2., 2., 2., 2., 3., 3., 3., 3., 3., 3., 3., 3., 3.}; + constexpr axom::IndexType NI = 3; + constexpr axom::IndexType NJ = 3; + constexpr axom::IndexType NK = 2; + mint::CurvilinearMesh mesh(NI, const_cast(x), + NJ, const_cast(y), + NK, const_cast(z)); + + testing::Test3D tester; + tester.test(mesh, "curvilinear3d.stl", false); + tester.test(mesh, "curvilinear3dB.stl", true); +} + +TEST(quest_stl_writer, unstructured3d) +{ + // Make mesh. + const double x[] = {0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1.}; + const double y[] = {1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2., 1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2.}; + const double z[] = {2., 2., 2., 2., 2., 2., 2., 2., 2., 3., 3., 3., 3., 3., 3., 3., 3., 3.}; + const axom::IndexType conn[] = { + 0,1,4,3,9,10,13,12, + 1,2,5,4,10,11,14,13, + 3,4,7,6,12,13,16,15, + 4,5,8,7,13,14,17,16 + }; + constexpr axom::IndexType nnodes = 18; + constexpr axom::IndexType ncells = 4; + mint::UnstructuredMesh mesh(mint::CellType::HEX, + ncells, // ncells + ncells, // cell_capacity + const_cast(conn), + nnodes, // nnodes + nnodes, // node_capacity + const_cast(x), + const_cast(y), + const_cast(z)); + mesh.initializeFaceConnectivity(); + + testing::Test3DUns tester; + tester.test(mesh, "unstructured3d.stl", false); + tester.test(mesh, "unstructured3dB.stl", true); +} + //------------------------------------------------------------------------------ int main(int argc, char* argv[]) { From 664355b2f4c230a7935aeb11c2703eb7f1710d40 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 19:43:32 -0700 Subject: [PATCH 09/15] Improvements for mesh tester. --- src/axom/quest/io/STLWriter.cpp | 43 +++++++++++++++++--- src/axom/quest/io/STLWriter.hpp | 10 +++++ src/tools/mesh_tester.cpp | 69 +++++++++++++++++++++++---------- 3 files changed, 96 insertions(+), 26 deletions(-) diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp index 5b9f4d9e49..7ec002670d 100644 --- a/src/axom/quest/io/STLWriter.cpp +++ b/src/axom/quest/io/STLWriter.cpp @@ -67,10 +67,30 @@ STLWriter::STLWriter(const std::string &filename, bool binary) : m_fileName(file { } +bool STLWriter::isTopologically2D(const mint::Mesh* mesh) const +{ + bool is2D = false; + if(mesh->getDimension() == 2) + { + is2D = true; + } + else if(mesh->getDimension() == 3) + { + // Heuristic for determining whether a mesh seems 2D independent of the coordinates. + if(mesh->getNumberOfFaces() == 0 && mesh->getNumberOfCells() > 0) + { + // 2D shapes in 3D don't seem to have faces in mint. Get the type of the first cell. + const auto ct = mesh->getCellType(0); + is2D = (ct == mint::CellType::TRIANGLE || ct == mint::CellType::QUAD); + } + } + return is2D; +} + IndexType STLWriter::getNumberOfTriangles(const mint::Mesh* mesh) const { IndexType ntri = 0; - if(mesh->getDimension() == 2) + if(isTopologically2D(mesh)) { for(IndexType cellId = 0; cellId < mesh->getNumberOfCells(); cellId++) { @@ -92,6 +112,8 @@ IndexType STLWriter::getNumberOfTriangles(const mint::Mesh* mesh) const int STLWriter::write(const mint::Mesh* mesh) { + using VectorType = axom::primal::Vector; + SLIC_ERROR_IF(mesh == nullptr, "mesh pointer is null!"); SLIC_ERROR_IF(m_fileName.length() <= 0, "STL filename is empty!"); SLIC_ERROR_IF(mesh->getDimension() < 2 || mesh->getDimension() > 3, "Input mesh must is not 2D/3D."); @@ -125,10 +147,11 @@ int STLWriter::write(const mint::Mesh* mesh) } // Write triangle data. - if(mesh->getDimension() == 2) + if(isTopologically2D(mesh)) { + // For meshes with cells that look 2D, we iterate over the cells. axom::Array nodes; - axom::StackArray N{{0., 0., 1.}}; + VectorType N = VectorType::make_vector(0., 0., 1.); double coords[3][3] = {{0., 0., 0.}, {0., 0., 0.}, {0., 0., 0.}}; for(axom::IndexType cellId = 0; cellId < mesh->getNumberOfCells(); cellId++) { @@ -143,14 +166,22 @@ int STLWriter::write(const mint::Mesh* mesh) { mesh->getNode(nodes[ti + 1], coords[1]); mesh->getNode(nodes[ti + 2], coords[2]); + + // If the cell is in 3D space, make a better normal. + if(mesh->getDimension() == 3) + { + const VectorType A(coords[0], 3); + const VectorType B(coords[1], 3); + const VectorType C(coords[2], 3); + N = VectorType::cross_product(B - A, C - A).unitVector(); + } + internal::writeTriangle(out, m_binary, coords, N); } } } else { - using VectorType = axom::primal::Vector; - // For value capture. std::ofstream *out_ptr = &out; const bool binary = m_binary; @@ -171,7 +202,7 @@ int STLWriter::write(const mint::Mesh* mesh) const VectorType A(coords[0], 3); const VectorType B(coords[1], 3); const VectorType C(coords[2], 3); - const VectorType N = VectorType::cross_product(B - A, C - A); + const VectorType N = VectorType::cross_product(B - A, C - A).unitVector(); internal::writeTriangle(*out_ptr, binary, coords, N); } diff --git a/src/axom/quest/io/STLWriter.hpp b/src/axom/quest/io/STLWriter.hpp index 4dfb5926dc..6875539e95 100644 --- a/src/axom/quest/io/STLWriter.hpp +++ b/src/axom/quest/io/STLWriter.hpp @@ -81,6 +81,16 @@ class STLWriter */ IndexType getNumberOfTriangles(const mint::Mesh* mesh) const; + /*! + * \brief Determines whether the input mesh looks topologically 2D since + * the mesh dimension seems to reflect the number of coordinate components. + * + * \param mesh The mesh to test. + * + * \return True if the mesh seems 2D, false otherwise. + */ + bool isTopologically2D(const mint::Mesh* mesh) const; + std::string m_fileName{"output.stl"}; bool m_binary{false}; }; diff --git a/src/tools/mesh_tester.cpp b/src/tools/mesh_tester.cpp index 165365c5d3..cff2b717cb 100644 --- a/src/tools/mesh_tester.cpp +++ b/src/tools/mesh_tester.cpp @@ -21,6 +21,7 @@ // _read_stl_include1_start #include "axom/quest/io/STLReader.hpp" +#include "axom/quest/io/STLWriter.hpp" // _read_stl_include1_end // _check_repair_include_start #include "axom/quest/MeshTester.hpp" @@ -91,10 +92,12 @@ enum RuntimePolicy struct Input { static const std::set s_validMethods; + static const std::set s_validFormats; static const std::map s_validPolicies; std::string stlInput {""}; - std::string vtkOutput {""}; + std::string fileOutput {""}; + std::string fileFormat {"vtk"}; std::string method {"uniform"}; RuntimePolicy policy {seq}; std::string annotationMode {"none"}; @@ -104,14 +107,15 @@ struct Input double intersectionThreshold {1e-08}; bool skipWeld {false}; bool verboseOutput {false}; + bool binary {false}; Input() = default; void parse(int argc, char** argv, axom::CLI::App& app); - std::string collisionsMeshName() { return vtkOutput + ".collisions.vtk"; } - std::string collisionsTextName() { return vtkOutput + ".collisions.txt"; } - std::string weldMeshName() { return vtkOutput + ".weld.vtk"; } + std::string collisionsMeshName() { return fileOutput + ".collisions." + fileFormat; } + std::string collisionsTextName() { return fileOutput + ".collisions.txt"; } + std::string weldMeshName() { return fileOutput + ".weld." + fileFormat; } private: void fixOutfilePath(); @@ -125,6 +129,11 @@ const std::set Input::s_validMethods({ "naive" }); +const std::set Input::s_validFormats({ + "stl", + "vtk" +}); + const std::map Input::s_validPolicies({ {"seq", seq} #ifdef AXOM_USE_RAJA @@ -185,14 +194,19 @@ void Input::parse(int argc, char** argv, axom::CLI::App& app) ->capture_default_str() ->transform(axom::CLI::CheckedTransformer(Input::s_validPolicies)); + app.add_option("-f, --format", fileFormat, "Output file format, one of: 'stl', 'vtk'") + ->capture_default_str() + ->check(axom::CLI::IsMember {Input::s_validFormats}); + app.add_option("-i,--infile", stlInput, "The STL input file")->required()->check(axom::CLI::ExistingFile); app.add_option("-o,--outfile", - vtkOutput, + fileOutput, "Output file name for collisions and welded mesh. \n" "Defaults to a file in the CWD with the input file name. \n" - "Collisions mesh will end with '.collisions.vtk' and \n" - "welded mesh will end with '.welded.vtk'."); + "Collisions mesh will end with '.collisions.{format}' and \n" + "welded mesh will end with '.welded.{format}', where {format} is\n" + "the selected file format."); app.add_option("--weldThresh", weldThreshold, "Distance threshold for welding vertices.") ->capture_default_str(); @@ -207,6 +221,10 @@ void Input::parse(int argc, char** argv, axom::CLI::App& app) skipWeld, "Don't weld vertices (useful for testing, not helpful otherwise)."); + app.add_flag("--binary", + binary, + "Write binary output files in supported formats."); + app.add_flag("-v,--verbose", verboseOutput, "Increase logging verbosity.")->capture_default_str(); #ifdef AXOM_USE_CALIPER @@ -282,20 +300,20 @@ void Input::fixOutfilePath() std::string outFileBase = futil::joinPath(futil::getCWD(), inFileStem); // set output file name when not provided - int sz = static_cast(vtkOutput.size()); + int sz = static_cast(fileOutput.size()); if(sz < 1) { - vtkOutput = outFileBase; - sz = static_cast(vtkOutput.size()); + fileOutput = outFileBase; + sz = static_cast(fileOutput.size()); } // ensure that output file does not end with '.vtk' if(sz > 4) { - std::string ext = vtkOutput.substr(sz - 4, 4); + std::string ext = fileOutput.substr(sz - 4, 4); if(ext == ".vtk" || ext == ".stl") { - vtkOutput = vtkOutput.substr(0, sz - ext.size()); + fileOutput = fileOutput.substr(0, sz - ext.size()); } } } @@ -314,7 +332,8 @@ void saveProblemFlagsToMesh(mint::Mesh* surface_mesh, const std::vector>& c, const std::vector& d); -bool writeAnnotatedMesh(mint::Mesh* surface_mesh, const std::string& outfile); +bool writeAnnotatedMesh(mint::Mesh* surface_mesh, const std::string& outfile, + const std::string &fileFormat, bool binary); bool writeCollisions(const std::vector>& c, const std::vector& d, @@ -544,9 +563,22 @@ void saveProblemFlagsToMesh(mint::Mesh* mesh, } } -bool writeAnnotatedMesh(mint::Mesh* surface_mesh, const std::string& outfile) +bool writeAnnotatedMesh(mint::Mesh* surface_mesh, const std::string& outfile, + const std::string &fileFormat, bool binary) { - return write_vtk(surface_mesh, outfile) == 0; + bool retval = false; + if(fileFormat == "vtk") + { + AXOM_ANNOTATE_SCOPE("save vtk mesh"); + retval = mint::write_vtk(surface_mesh, outfile) == 0; + } + else if(fileFormat == "stl") + { + AXOM_ANNOTATE_SCOPE("save stl mesh"); + // NOTE: The STL writer is co-located with the STL reader in quest. + retval = quest::write_stl(surface_mesh, outfile, binary) == 0; + } + return retval; } bool writeCollisions(const std::vector>& c, @@ -893,7 +925,7 @@ int main(int argc, char** argv) saveProblemFlagsToMesh(surface_mesh, collisions, degenerate); - if(!writeAnnotatedMesh(surface_mesh, params.collisionsMeshName())) + if(!writeAnnotatedMesh(surface_mesh, params.collisionsMeshName(), params.fileFormat, params.binary)) { SLIC_ERROR("Couldn't write results to " << params.collisionsMeshName()); } @@ -951,10 +983,7 @@ int main(int argc, char** argv) // _report_watertight_end SLIC_INFO("Testing for watertightness took " << timer2.elapsedTimeInSec() << " seconds."); - { - AXOM_ANNOTATE_SCOPE("save vtk mesh"); - mint::write_vtk(surface_mesh, params.weldMeshName()); - } + writeAnnotatedMesh(surface_mesh, params.weldMeshName(), params.fileFormat, params.binary); } // Delete the mesh From 43c8d35dbd8454ef5c480cae689d3a52c7fbdef7 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 19:44:05 -0700 Subject: [PATCH 10/15] Improve comments and uncomment removeFile. --- src/axom/quest/tests/quest_stl_writer.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/axom/quest/tests/quest_stl_writer.cpp b/src/axom/quest/tests/quest_stl_writer.cpp index 0126939d91..a17df6ca4b 100644 --- a/src/axom/quest/tests/quest_stl_writer.cpp +++ b/src/axom/quest/tests/quest_stl_writer.cpp @@ -20,7 +20,8 @@ #include #include -// Uncomment this line to write new baseline data to stdout. +// Uncomment the line below to write new baseline data to stdout. Then paste new +// baselines into the various baseline methods. // #define AXOM_SET_BASELINES // namespace aliases @@ -197,7 +198,7 @@ struct Test3D #endif } -// axom::utilities::filesystem::removeFile(filename); + axom::utilities::filesystem::removeFile(filename); } }; @@ -351,12 +352,15 @@ TEST(quest_stl_writer, unstructured3d) const double x[] = {0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1.}; const double y[] = {1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2., 1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2.}; const double z[] = {2., 2., 2., 2., 2., 2., 2., 2., 2., 3., 3., 3., 3., 3., 3., 3., 3., 3.}; + // clang-format off const axom::IndexType conn[] = { 0,1,4,3,9,10,13,12, 1,2,5,4,10,11,14,13, 3,4,7,6,12,13,16,15, 4,5,8,7,13,14,17,16 }; + // clang-format on + constexpr axom::IndexType nnodes = 18; constexpr axom::IndexType ncells = 4; mint::UnstructuredMesh mesh(mint::CellType::HEX, From 96df5a3401c6415b779a102f3888328f3f937978 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 19:44:53 -0700 Subject: [PATCH 11/15] make style --- src/axom/quest/io/STLWriter.cpp | 97 +++++++++--------- src/axom/quest/io/STLWriter.hpp | 8 +- src/axom/quest/tests/quest_stl_writer.cpp | 115 +++++++++++++++------- src/tools/mesh_tester.cpp | 16 +-- 4 files changed, 142 insertions(+), 94 deletions(-) diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp index 7ec002670d..45ef9fd6d9 100644 --- a/src/axom/quest/io/STLWriter.cpp +++ b/src/axom/quest/io/STLWriter.cpp @@ -30,44 +30,45 @@ namespace internal template void writeTriangle(std::ofstream &out, bool binary, double coords[3][3], const NormalType &N) { - if(binary) - { - using float32 = float; - using uint16 = unsigned short; - float32 n32[3], coords32[3][3]; - for(int comp = 0; comp < 3; comp++) - { - n32[comp] = static_cast(N[comp]); - coords32[0][comp] = static_cast(coords[0][comp]); - coords32[1][comp] = static_cast(coords[1][comp]); - coords32[2][comp] = static_cast(coords[2][comp]); - } - const uint16 attr = 0x7fff; - out.write(reinterpret_cast(n32), 3 * sizeof(float32)); - out.write(reinterpret_cast(coords32[0]), 3 * sizeof(float32)); - out.write(reinterpret_cast(coords32[1]), 3 * sizeof(float32)); - out.write(reinterpret_cast(coords32[2]), 3 * sizeof(float32)); - out.write(reinterpret_cast(&attr), sizeof(uint16)); - } - else - { - out << "\t facet normal " << N[0] << " " << N[1] << " " << N[2] << "\n"; - out << "\t\t outer loop\n"; - out << "\t\t\t vertex " << coords[0][0] << " " << coords[0][1] << " " << coords[0][2] << "\n"; - out << "\t\t\t vertex " << coords[1][0] << " " << coords[1][1] << " " << coords[1][2] << "\n"; - out << "\t\t\t vertex " << coords[2][0] << " " << coords[2][1] << " " << coords[2][2] << "\n"; - out << "\t\t endloop\n"; - out << "\t endfacet\n"; - } + if(binary) + { + using float32 = float; + using uint16 = unsigned short; + float32 n32[3], coords32[3][3]; + for(int comp = 0; comp < 3; comp++) + { + n32[comp] = static_cast(N[comp]); + coords32[0][comp] = static_cast(coords[0][comp]); + coords32[1][comp] = static_cast(coords[1][comp]); + coords32[2][comp] = static_cast(coords[2][comp]); + } + const uint16 attr = 0x7fff; + out.write(reinterpret_cast(n32), 3 * sizeof(float32)); + out.write(reinterpret_cast(coords32[0]), 3 * sizeof(float32)); + out.write(reinterpret_cast(coords32[1]), 3 * sizeof(float32)); + out.write(reinterpret_cast(coords32[2]), 3 * sizeof(float32)); + out.write(reinterpret_cast(&attr), sizeof(uint16)); + } + else + { + out << "\t facet normal " << N[0] << " " << N[1] << " " << N[2] << "\n"; + out << "\t\t outer loop\n"; + out << "\t\t\t vertex " << coords[0][0] << " " << coords[0][1] << " " << coords[0][2] << "\n"; + out << "\t\t\t vertex " << coords[1][0] << " " << coords[1][1] << " " << coords[1][2] << "\n"; + out << "\t\t\t vertex " << coords[2][0] << " " << coords[2][1] << " " << coords[2][2] << "\n"; + out << "\t\t endloop\n"; + out << "\t endfacet\n"; + } } -} // end namespace internal +} // end namespace internal -STLWriter::STLWriter(const std::string &filename, bool binary) : m_fileName(filename), m_binary(binary) -{ -} +STLWriter::STLWriter(const std::string &filename, bool binary) + : m_fileName(filename) + , m_binary(binary) +{ } -bool STLWriter::isTopologically2D(const mint::Mesh* mesh) const +bool STLWriter::isTopologically2D(const mint::Mesh *mesh) const { bool is2D = false; if(mesh->getDimension() == 2) @@ -87,7 +88,7 @@ bool STLWriter::isTopologically2D(const mint::Mesh* mesh) const return is2D; } -IndexType STLWriter::getNumberOfTriangles(const mint::Mesh* mesh) const +IndexType STLWriter::getNumberOfTriangles(const mint::Mesh *mesh) const { IndexType ntri = 0; if(isTopologically2D(mesh)) @@ -102,23 +103,25 @@ IndexType STLWriter::getNumberOfTriangles(const mint::Mesh* mesh) const axom::ReduceSum ntri_reduce(0); axom::mint::for_all_faces( mesh, - AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType* AXOM_UNUSED_PARAM(nodes), IndexType N) { - ntri_reduce += (N - 2); - }); + AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), + const IndexType *AXOM_UNUSED_PARAM(nodes), + IndexType N) { ntri_reduce += (N - 2); }); ntri = ntri_reduce.get(); } return ntri; } -int STLWriter::write(const mint::Mesh* mesh) +int STLWriter::write(const mint::Mesh *mesh) { using VectorType = axom::primal::Vector; SLIC_ERROR_IF(mesh == nullptr, "mesh pointer is null!"); SLIC_ERROR_IF(m_fileName.length() <= 0, "STL filename is empty!"); - SLIC_ERROR_IF(mesh->getDimension() < 2 || mesh->getDimension() > 3, "Input mesh must is not 2D/3D."); + SLIC_ERROR_IF(mesh->getDimension() < 2 || mesh->getDimension() > 3, + "Input mesh must is not 2D/3D."); - std::ofstream out(m_fileName.c_str(), m_binary ? (std::ofstream::out | std::ofstream::binary) : std::ofstream::out); + std::ofstream out(m_fileName.c_str(), + m_binary ? (std::ofstream::out | std::ofstream::binary) : std::ofstream::out); if(!out.is_open()) { SLIC_WARNING("cannot write to file [" << m_fileName << "]"); @@ -135,11 +138,11 @@ int STLWriter::write(const mint::Mesh* mesh) // Copy in string (without terminator) const char *msg = "STL Binary File Written By Axom"; memcpy(header, msg, strlen(msg)); - out.write(reinterpret_cast(header), STL_HEADER_SIZE); + out.write(reinterpret_cast(header), STL_HEADER_SIZE); // Write number of triangles std::uint32_t ntri = static_cast(getNumberOfTriangles(mesh)); - out.write(reinterpret_cast(&ntri), sizeof(std::uint32_t)); + out.write(reinterpret_cast(&ntri), sizeof(std::uint32_t)); } else { @@ -188,7 +191,7 @@ int STLWriter::write(const mint::Mesh* mesh) axom::mint::for_all_faces( mesh, - AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType* nodes, IndexType nnodes) { + AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType *nodes, IndexType nnodes) { // Iterate over the face like a triangle fan. double coords[3][3] = {{0., 0., 0.}, {0., 0., 0.}, {0., 0., 0.}}; mesh->getNode(nodes[0], coords[0]); @@ -218,10 +221,10 @@ int STLWriter::write(const mint::Mesh* mesh) return 0; } -int write_stl(const mint::Mesh* mesh, const std::string &filename, bool binary) +int write_stl(const mint::Mesh *mesh, const std::string &filename, bool binary) { - STLWriter w(filename, binary); - return w.write(mesh); + STLWriter w(filename, binary); + return w.write(mesh); } } // namespace quest diff --git a/src/axom/quest/io/STLWriter.hpp b/src/axom/quest/io/STLWriter.hpp index 6875539e95..1131583770 100644 --- a/src/axom/quest/io/STLWriter.hpp +++ b/src/axom/quest/io/STLWriter.hpp @@ -44,7 +44,7 @@ class STLWriter * \param filename The name of the file to write. * \param binary Whether or not to write a binary STL file. */ - STLWriter(const std::string &filename, bool binary = false); + STLWriter(const std::string& filename, bool binary = false); /*! * \brief Destructor. @@ -91,8 +91,8 @@ class STLWriter */ bool isTopologically2D(const mint::Mesh* mesh) const; - std::string m_fileName{"output.stl"}; - bool m_binary{false}; + std::string m_fileName {"output.stl"}; + bool m_binary {false}; }; /*! @@ -104,7 +104,7 @@ class STLWriter * * \return 0 on success; non-zero otherwise. */ -int write_stl(const mint::Mesh* mesh, const std::string &filename, bool binary = false); +int write_stl(const mint::Mesh* mesh, const std::string& filename, bool binary = false); } // namespace quest } // namespace axom diff --git a/src/axom/quest/tests/quest_stl_writer.cpp b/src/axom/quest/tests/quest_stl_writer.cpp index a17df6ca4b..d88bbb42ba 100644 --- a/src/axom/quest/tests/quest_stl_writer.cpp +++ b/src/axom/quest/tests/quest_stl_writer.cpp @@ -31,13 +31,13 @@ namespace quest = axom::quest; namespace testing { -void writeArray(const axom::Array& vec, const std::string& var_name = "v") +void writeArray(const axom::Array &vec, const std::string &var_name = "v") { axom::fmt::print("axom::Array {} = {{{}}};\n", var_name, axom::fmt::join(vec, ", ")); } /// Convert mesh coordinates into arrays that can be easily compared. -void getCoordinates(const mint::Mesh &mesh, axom::Array &xc, axom::Array & yc) +void getCoordinates(const mint::Mesh &mesh, axom::Array &xc, axom::Array &yc) { for(axom::IndexType cellId = 0; cellId < mesh.getNumberOfCells(); cellId++) { @@ -99,12 +99,14 @@ struct Test2D { axom::Array baselineXCoordinates() { - return axom::Array{{0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; + return axom::Array {{0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, + 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; } axom::Array baselineYCoordinates() { - return axom::Array{{1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; + return axom::Array {{1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, + 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; } void test(const mint::Mesh &mesh, const std::string &filename, bool binary) @@ -151,17 +153,35 @@ struct Test3D { virtual axom::Array baselineXCoordinates() { - return axom::Array{{0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; + return axom::Array { + {0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, + 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, + 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, + 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, + 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, + 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; } virtual axom::Array baselineYCoordinates() { - return axom::Array{{1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; + return axom::Array { + {1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, + 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, + 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, + 1.5, 1.5, 2, 1.5, 2, 2, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, + 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; } virtual axom::Array baselineZCoordinates() { - return axom::Array{{2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}}; + return axom::Array { + {2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, + 2, 3, 3, 2, 3, 2, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, + 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}}; } void test(const mint::Mesh &mesh, const std::string &filename, bool binary) @@ -210,21 +230,39 @@ struct Test3DUns : public Test3D { virtual axom::Array baselineXCoordinates() override { - return axom::Array{{0, 0, 0.5, 0, 0.5, 0.5, 0.5, 0.5, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 1, 0.5, 1, 1, 1, 1, 0.5, 1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0, 0.5, 0, 0.5, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 1, 0.5, 1, 1, 0.5, 1, 1, 0.5, 1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; + return axom::Array { + {0, 0, 0.5, 0, 0.5, 0.5, 0.5, 0.5, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, + 0.5, 0.5, 1, 0.5, 1, 1, 1, 1, 0.5, 1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 1, 1, 1, 1, 1, 1, 0, 0, 0.5, 0, 0.5, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, + 0, 0, 0, 0, 0, 0, 0.5, 0.5, 1, 0.5, 1, 1, 0.5, 1, 1, 0.5, 1, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 0, 0.5, 0.5, 0, 0.5, 0, + 0.5, 1, 1, 0.5, 1, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5, + 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 1, 1, 0.5, 1, 0.5}}; } virtual axom::Array baselineYCoordinates() override { - return axom::Array{{1, 1.5, 1.5, 1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1.5, 1.5, 1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1.5, 1, 1.5, 1, 1, 1.5, 1.5, 1, 1.5, 1, 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 2, 2, 1.5, 2, 1.5, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; + return axom::Array { + {1, 1.5, 1.5, 1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1, 1.5, 1.5, + 1, 1.5, 1.5, 1, 1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 1.5, 1, 1.5, 1, + 1, 1.5, 1.5, 1, 1.5, 1, 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, + 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, + 1.5, 2, 2, 1.5, 2, 1.5, 1.5, 2, 2, 1.5, 2, 1.5, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 1, 1, 1.5, 1, 1.5, 1.5, 1, 1, 1.5, 1, 1.5, 1.5, + 1.5, 1.5, 2, 1.5, 2, 2, 1.5, 1.5, 2, 1.5, 2, 2}}; } virtual axom::Array baselineZCoordinates() override { - return axom::Array{{2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}}; + return axom::Array { + {2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 3, 3, 2, 3, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, + 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 3, 3, 2, 3, 2, + 2, 2, 2, 2, 2, 2, 3, 3, 2, 3, 2, 2, 2, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 3, 3, 3, 2, 3, 2, 2, + 3, 3, 2, 3, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}}; } }; -} // end namespace testing +} // end namespace testing //------------------------------------------------------------------------------ // UNIT TESTS @@ -276,19 +314,18 @@ TEST(quest_stl_writer, unstructured2d) // Make mesh. const double x[] = {0., 0.5, 1., 0., 0.5, 1., 0., 0.5, 1.}; const double y[] = {1., 1., 1., 1.5, 1.5, 1.5, 2., 2., 2.}; - const axom::IndexType conn[] = { - 0, 1, 4, 0, 4, 3, 1, 2, 5, 1, 5, 4, 3, 4, 7, 3, 7, 6, 4, 5, 8, 4, 8, 7 - }; + const axom::IndexType conn[] = {0, 1, 4, 0, 4, 3, 1, 2, 5, 1, 5, 4, + 3, 4, 7, 3, 7, 6, 4, 5, 8, 4, 8, 7}; constexpr axom::IndexType nnodes = 9; constexpr axom::IndexType numTriangles = 8; mint::UnstructuredMesh mesh(mint::CellType::TRIANGLE, - numTriangles, // ncells - numTriangles, // cell_capacity - const_cast(conn), - nnodes, // nnodes - nnodes, // node_capacity - const_cast(x), - const_cast(y)); + numTriangles, // ncells + numTriangles, // cell_capacity + const_cast(conn), + nnodes, // nnodes + nnodes, // node_capacity + const_cast(x), + const_cast(y)); testing::Test2D tester; tester.test(mesh, "unstructured2d.stl", false); @@ -319,9 +356,12 @@ TEST(quest_stl_writer, rectilinear3d) constexpr axom::IndexType NI = 3; constexpr axom::IndexType NJ = 3; constexpr axom::IndexType NK = 2; - mint::RectilinearMesh mesh(NI, const_cast(x), - NJ, const_cast(y), - NK, const_cast(z)); + mint::RectilinearMesh mesh(NI, + const_cast(x), + NJ, + const_cast(y), + NK, + const_cast(z)); testing::Test3D tester; tester.test(mesh, "rectilinear3d.stl", false); @@ -337,9 +377,12 @@ TEST(quest_stl_writer, curvilinear3d) constexpr axom::IndexType NI = 3; constexpr axom::IndexType NJ = 3; constexpr axom::IndexType NK = 2; - mint::CurvilinearMesh mesh(NI, const_cast(x), - NJ, const_cast(y), - NK, const_cast(z)); + mint::CurvilinearMesh mesh(NI, + const_cast(x), + NJ, + const_cast(y), + NK, + const_cast(z)); testing::Test3D tester; tester.test(mesh, "curvilinear3d.stl", false); @@ -364,14 +407,14 @@ TEST(quest_stl_writer, unstructured3d) constexpr axom::IndexType nnodes = 18; constexpr axom::IndexType ncells = 4; mint::UnstructuredMesh mesh(mint::CellType::HEX, - ncells, // ncells - ncells, // cell_capacity - const_cast(conn), - nnodes, // nnodes - nnodes, // node_capacity - const_cast(x), - const_cast(y), - const_cast(z)); + ncells, // ncells + ncells, // cell_capacity + const_cast(conn), + nnodes, // nnodes + nnodes, // node_capacity + const_cast(x), + const_cast(y), + const_cast(z)); mesh.initializeFaceConnectivity(); testing::Test3DUns tester; @@ -380,7 +423,7 @@ TEST(quest_stl_writer, unstructured3d) } //------------------------------------------------------------------------------ -int main(int argc, char* argv[]) +int main(int argc, char *argv[]) { ::testing::InitGoogleTest(&argc, argv); axom::slic::SimpleLogger logger; diff --git a/src/tools/mesh_tester.cpp b/src/tools/mesh_tester.cpp index cff2b717cb..fa74455ae9 100644 --- a/src/tools/mesh_tester.cpp +++ b/src/tools/mesh_tester.cpp @@ -221,9 +221,7 @@ void Input::parse(int argc, char** argv, axom::CLI::App& app) skipWeld, "Don't weld vertices (useful for testing, not helpful otherwise)."); - app.add_flag("--binary", - binary, - "Write binary output files in supported formats."); + app.add_flag("--binary", binary, "Write binary output files in supported formats."); app.add_flag("-v,--verbose", verboseOutput, "Increase logging verbosity.")->capture_default_str(); @@ -332,8 +330,10 @@ void saveProblemFlagsToMesh(mint::Mesh* surface_mesh, const std::vector>& c, const std::vector& d); -bool writeAnnotatedMesh(mint::Mesh* surface_mesh, const std::string& outfile, - const std::string &fileFormat, bool binary); +bool writeAnnotatedMesh(mint::Mesh* surface_mesh, + const std::string& outfile, + const std::string& fileFormat, + bool binary); bool writeCollisions(const std::vector>& c, const std::vector& d, @@ -563,8 +563,10 @@ void saveProblemFlagsToMesh(mint::Mesh* mesh, } } -bool writeAnnotatedMesh(mint::Mesh* surface_mesh, const std::string& outfile, - const std::string &fileFormat, bool binary) +bool writeAnnotatedMesh(mint::Mesh* surface_mesh, + const std::string& outfile, + const std::string& fileFormat, + bool binary) { bool retval = false; if(fileFormat == "vtk") From 2ba843649ce5b7bfb29f93383099c3dd23305335 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Tue, 24 Jun 2025 19:57:30 -0700 Subject: [PATCH 12/15] Added include file --- src/axom/quest/io/STLWriter.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp index 45ef9fd6d9..d7dd81a896 100644 --- a/src/axom/quest/io/STLWriter.cpp +++ b/src/axom/quest/io/STLWriter.cpp @@ -9,6 +9,7 @@ #include "axom/mint/execution/interface.hpp" #include "axom/primal/geometry/Vector.hpp" +#include #include namespace axom From ac77d3994aa9740c70b82aaedd1c441cd0c0b214 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Wed, 25 Jun 2025 15:59:33 -0700 Subject: [PATCH 13/15] Fix CUDA compilation. --- src/axom/quest/io/STLWriter.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/axom/quest/io/STLWriter.hpp b/src/axom/quest/io/STLWriter.hpp index 1131583770..7d0adfb939 100644 --- a/src/axom/quest/io/STLWriter.hpp +++ b/src/axom/quest/io/STLWriter.hpp @@ -71,7 +71,11 @@ class STLWriter */ int write(const mint::Mesh* mesh); +// The following members are protected (unless using CUDA) +#if !defined(__CUDACC__) protected: +#endif + /*! * \brief Compute the number of triangles produced for the input mesh. * From 6bc088914a68e40c2fdab3ff786a90fd7ce7c298 Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Wed, 25 Jun 2025 18:22:45 -0700 Subject: [PATCH 14/15] Fix warning string. --- src/axom/quest/io/STLWriter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp index d7dd81a896..424da17208 100644 --- a/src/axom/quest/io/STLWriter.cpp +++ b/src/axom/quest/io/STLWriter.cpp @@ -119,7 +119,7 @@ int STLWriter::write(const mint::Mesh *mesh) SLIC_ERROR_IF(mesh == nullptr, "mesh pointer is null!"); SLIC_ERROR_IF(m_fileName.length() <= 0, "STL filename is empty!"); SLIC_ERROR_IF(mesh->getDimension() < 2 || mesh->getDimension() > 3, - "Input mesh must is not 2D/3D."); + "Input mesh is not 2D/3D."); std::ofstream out(m_fileName.c_str(), m_binary ? (std::ofstream::out | std::ofstream::binary) : std::ofstream::out); From ca055eca6a446dd34c66a68acd6c0a80c26169dc Mon Sep 17 00:00:00 2001 From: Brad Whitlock Date: Fri, 27 Jun 2025 12:30:19 -0700 Subject: [PATCH 15/15] Address review comments. --- src/axom/quest/io/STLWriter.cpp | 67 +++++++++++++++++++-------------- src/axom/quest/io/STLWriter.hpp | 17 ++++----- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/axom/quest/io/STLWriter.cpp b/src/axom/quest/io/STLWriter.cpp index 424da17208..a6a2474fe4 100644 --- a/src/axom/quest/io/STLWriter.cpp +++ b/src/axom/quest/io/STLWriter.cpp @@ -34,7 +34,6 @@ void writeTriangle(std::ofstream &out, bool binary, double coords[3][3], const N if(binary) { using float32 = float; - using uint16 = unsigned short; float32 n32[3], coords32[3][3]; for(int comp = 0; comp < 3; comp++) { @@ -43,12 +42,14 @@ void writeTriangle(std::ofstream &out, bool binary, double coords[3][3], const N coords32[1][comp] = static_cast(coords[1][comp]); coords32[2][comp] = static_cast(coords[2][comp]); } - const uint16 attr = 0x7fff; + // The attribute is sometimes used as colors. Set bits to white. + // See https://en.wikipedia.org/wiki/STL_(file_format). + const std::uint16_t attr = 0x7fff; out.write(reinterpret_cast(n32), 3 * sizeof(float32)); out.write(reinterpret_cast(coords32[0]), 3 * sizeof(float32)); out.write(reinterpret_cast(coords32[1]), 3 * sizeof(float32)); out.write(reinterpret_cast(coords32[2]), 3 * sizeof(float32)); - out.write(reinterpret_cast(&attr), sizeof(uint16)); + out.write(reinterpret_cast(&attr), sizeof(std::uint16_t)); } else { @@ -65,45 +66,50 @@ void writeTriangle(std::ofstream &out, bool binary, double coords[3][3], const N } // end namespace internal STLWriter::STLWriter(const std::string &filename, bool binary) - : m_fileName(filename) + : m_mesh(nullptr) + , m_fileName(filename) , m_binary(binary) { } -bool STLWriter::isTopologically2D(const mint::Mesh *mesh) const +bool STLWriter::isTopologically2D() const { + SLIC_ERROR_IF(m_mesh == nullptr, "mesh pointer is null!"); + bool is2D = false; - if(mesh->getDimension() == 2) + if(m_mesh->getDimension() == 2) { is2D = true; } - else if(mesh->getDimension() == 3) + else if(m_mesh->getDimension() == 3) { - // Heuristic for determining whether a mesh seems 2D independent of the coordinates. - if(mesh->getNumberOfFaces() == 0 && mesh->getNumberOfCells() > 0) + // Heuristic for determining whether a m_mesh seems 2D independent of the coordinates. + if(m_mesh->getNumberOfFaces() == 0 && m_mesh->getNumberOfCells() > 0) { // 2D shapes in 3D don't seem to have faces in mint. Get the type of the first cell. - const auto ct = mesh->getCellType(0); + const auto ct = m_mesh->getCellType(0); is2D = (ct == mint::CellType::TRIANGLE || ct == mint::CellType::QUAD); } } return is2D; } -IndexType STLWriter::getNumberOfTriangles(const mint::Mesh *mesh) const +IndexType STLWriter::getNumberOfTriangles() const { + SLIC_ERROR_IF(m_mesh == nullptr, "mesh pointer is null!"); + IndexType ntri = 0; - if(isTopologically2D(mesh)) + if(isTopologically2D()) { - for(IndexType cellId = 0; cellId < mesh->getNumberOfCells(); cellId++) + for(IndexType cellId = 0; cellId < m_mesh->getNumberOfCells(); cellId++) { - ntri += (mesh->getNumberOfCellNodes(cellId) - 2); + ntri += (m_mesh->getNumberOfCellNodes(cellId) - 2); } } - else if(mesh->getDimension() == 3) + else if(m_mesh->getDimension() == 3) { axom::ReduceSum ntri_reduce(0); axom::mint::for_all_faces( - mesh, + m_mesh, AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType *AXOM_UNUSED_PARAM(nodes), IndexType N) { ntri_reduce += (N - 2); }); @@ -118,8 +124,10 @@ int STLWriter::write(const mint::Mesh *mesh) SLIC_ERROR_IF(mesh == nullptr, "mesh pointer is null!"); SLIC_ERROR_IF(m_fileName.length() <= 0, "STL filename is empty!"); - SLIC_ERROR_IF(mesh->getDimension() < 2 || mesh->getDimension() > 3, - "Input mesh is not 2D/3D."); + SLIC_ERROR_IF(mesh->getDimension() < 2 || mesh->getDimension() > 3, "Input mesh is not 2D/3D."); + + // Save mesh pointer + m_mesh = mesh; std::ofstream out(m_fileName.c_str(), m_binary ? (std::ofstream::out | std::ofstream::binary) : std::ofstream::out); @@ -142,7 +150,7 @@ int STLWriter::write(const mint::Mesh *mesh) out.write(reinterpret_cast(header), STL_HEADER_SIZE); // Write number of triangles - std::uint32_t ntri = static_cast(getNumberOfTriangles(mesh)); + std::uint32_t ntri = static_cast(getNumberOfTriangles()); out.write(reinterpret_cast(&ntri), sizeof(std::uint32_t)); } else @@ -151,28 +159,28 @@ int STLWriter::write(const mint::Mesh *mesh) } // Write triangle data. - if(isTopologically2D(mesh)) + if(isTopologically2D()) { // For meshes with cells that look 2D, we iterate over the cells. axom::Array nodes; VectorType N = VectorType::make_vector(0., 0., 1.); double coords[3][3] = {{0., 0., 0.}, {0., 0., 0.}, {0., 0., 0.}}; - for(axom::IndexType cellId = 0; cellId < mesh->getNumberOfCells(); cellId++) + for(axom::IndexType cellId = 0; cellId < m_mesh->getNumberOfCells(); cellId++) { // Get nodes for this cell. - nodes.resize(mesh->getNumberOfCellNodes(cellId)); - const auto nnodes = mesh->getCellNodeIDs(cellId, nodes.data()); + nodes.resize(m_mesh->getNumberOfCellNodes(cellId)); + const auto nnodes = m_mesh->getCellNodeIDs(cellId, nodes.data()); // Iterate over the face like a triangle fan. - mesh->getNode(nodes[0], coords[0]); + m_mesh->getNode(nodes[0], coords[0]); const IndexType ntri = nnodes - 2; for(IndexType ti = 0; ti < ntri; ti++) { - mesh->getNode(nodes[ti + 1], coords[1]); - mesh->getNode(nodes[ti + 2], coords[2]); + m_mesh->getNode(nodes[ti + 1], coords[1]); + m_mesh->getNode(nodes[ti + 2], coords[2]); // If the cell is in 3D space, make a better normal. - if(mesh->getDimension() == 3) + if(m_mesh->getDimension() == 3) { const VectorType A(coords[0], 3); const VectorType B(coords[1], 3); @@ -191,8 +199,11 @@ int STLWriter::write(const mint::Mesh *mesh) const bool binary = m_binary; axom::mint::for_all_faces( - mesh, + m_mesh, AXOM_LAMBDA(IndexType AXOM_UNUSED_PARAM(faceID), const IndexType *nodes, IndexType nnodes) { + // NOTE: Here in the lambda, we use "mesh" instead of "m_mesh" so we do + // not capture STLWriter's "this" pointer. + // Iterate over the face like a triangle fan. double coords[3][3] = {{0., 0., 0.}, {0., 0., 0.}, {0., 0., 0.}}; mesh->getNode(nodes[0], coords[0]); diff --git a/src/axom/quest/io/STLWriter.hpp b/src/axom/quest/io/STLWriter.hpp index 7d0adfb939..d3046cbb6f 100644 --- a/src/axom/quest/io/STLWriter.hpp +++ b/src/axom/quest/io/STLWriter.hpp @@ -20,7 +20,8 @@ namespace quest /*! * \class STLWriter * - * \brief A simple STL writer for the faces of Mint meshes. + * \brief A simple STL writer for a collection of triangular faces with + * limited support for planar polygons * * STL (STereoLithography) is a common file format for triangle meshes. * It encodes a "soup of triangles" by explicitly listing the coordinate @@ -79,22 +80,18 @@ class STLWriter /*! * \brief Compute the number of triangles produced for the input mesh. * - * \param mesh The input mesh. - * * \return The number of triangles. */ - IndexType getNumberOfTriangles(const mint::Mesh* mesh) const; + IndexType getNumberOfTriangles() const; /*! - * \brief Determines whether the input mesh looks topologically 2D since - * the mesh dimension seems to reflect the number of coordinate components. - * - * \param mesh The mesh to test. + * \brief Determines whether the input mesh is topologically 2D. * - * \return True if the mesh seems 2D, false otherwise. + * \return True if the mesh is topologically 2D, false otherwise. */ - bool isTopologically2D(const mint::Mesh* mesh) const; + bool isTopologically2D() const; + const mint::Mesh* m_mesh {nullptr}; std::string m_fileName {"output.stl"}; bool m_binary {false}; };