diff --git a/.vscode/settings.dev.json b/.vscode/settings.dev.json index b90d07d..5303634 100644 --- a/.vscode/settings.dev.json +++ b/.vscode/settings.dev.json @@ -5,5 +5,15 @@ "testMate.cpp.test.executables": "{build,build-dbg,Build,BUILD,out,Out,OUT}/**/*{test,Test,TEST}*", "testMate.cpp.test.workingDirectory": "${workspaceFolder}", "extensions.ignoreRecommendations": true, - "C_Cpp.intelliSenseEngine": "disabled" + "C_Cpp.intelliSenseEngine": "disabled", + "clangd.arguments": ["-log=verbose", + "-pretty", + "--background-index", + "--query-driver=/usr/bin/g++" + ], + "clangd.fallbackFlags": ["-std=c++17"], + "lldb.showDisassembly": "never", + "lldb.launch.preRunCommands": [ + "settings set target.import-std-module true" + ] } diff --git a/CMakePresets.json b/CMakePresets.json index 6cfa514..769d22e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -69,7 +69,8 @@ "CMAKE_FIND_ROOT_PATH": "/usr/local", "TESSELLATOR_ENABLE_TESTS": "ON", "TESSELLATOR_ENABLE_CGAL": "ON", - "DOCKER_EXPORT_COMPILE_COMMANDS": "ON" + "DOCKER_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_CXX_FLAGS_INIT": "-g3" }, "environment": { "EIGEN3_INCLUDE_DIR": "/usr/include/eigen3" diff --git a/README.md b/README.md index b777cec..c920511 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ The main binary is `tessellator`, which uses a tessellator json format, which wi ``` ## JSON Format -The two main entries are as follows: +The main entries are as follows: ### `` This object must always be present and contains the structure of the grid, which will be used to slice and adjust the mesh provided. It must contain one of these two sets of entries: - ``: is an array of three positive integers which indicate the number of cells in each Cartesian direction. In case of having this entry, it also must contain a ``: - - `` is represented by an array which contairs two triplets of integers, representing the minimum and maximum values of the gread in each cartesian direction. + - `` is represented by an array which contains two triplets of integers, representing the minimum and maximum values of the grid in each cartesian direction. ```json "grid": { @@ -80,17 +80,100 @@ This object must always be present and contains the structure of the grid, which } ``` -### `` -This contains the information about the mesh file. It must contain the following entry: +### `` or `` +This contains the information about the mesh file(s). You can specify a single object or multiple objects: -- `filename`: with an string containing the name of the mesh file. Its location is relative to that of the json file. - - Example: +**Single object:** +- `filename`: A string containing the name of the mesh file. Its location is relative to that of the json file. ```json "object": {"filename": "thinCylinder.stl"} ``` +**Multiple objects:** +- `objects`: An array of object definitions. Each object can have: + - `filename`: (required) The mesh file name, relative to the JSON file location + - `group`: (optional) Group name for the object (defaults to filename without extension) + - `mesher`: (optional) Override the global mesher settings for this specific object + +```json + "objects": [ + {"filename": "object1.stl", "group": "group1"}, + {"filename": "object2.stl", "group": "group2", "mesher": {"type": "conformal"}} + ] +``` + +### `` +This optional entry configures the meshing algorithm and its options. If not specified, the staircase mesher is used with default options. + +**Mesher types:** +- `staircase` (default): Generates staircased meshes from geometric inputs +- `conformal`: Creates conformal meshes with fixed-distance grid plane intersections + +**Mesher options:** + +For **staircase** mesher: +- `compress`: (boolean, default: false) Enables surface compression to merge adjacent coplanar quads into larger surfaces + +For **conformal** mesher: +- `edgePoints`: Controls edge point snapping behavior +- `forbiddenLength`: Minimum length threshold for snapping + +**Global options:** +- `exportGrid`: (boolean, default: true) Controls whether to export the grid file + +Example with staircase mesher and compression enabled: +```json + "mesher": { + "type": "staircase", + "options": { + "compress": true, + "exportGrid": true + } + } +``` + +Example with conformal mesher: +```json + "mesher": { + "type": "conformal", + "options": { + "edgePoints": true, + "forbiddenLength": 0.001 + } + } +``` + +### Output Files +The tessellator generates output files with the following naming convention: +- `{group_name}.tessellator.str.vtk` - Staircase meshed object +- `{group_name}.tessellator.cmsh.vtk` - Conformal meshed object +- `{basename}.tessellator.grid.vtk` - Grid file (if `exportGrid` is true) + +### Complete Example +```json +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "objects": [ + {"filename": "sphere.stl"}, + {"filename": "cylinder.stl", "mesher": {"type": "conformal"}} + ], + "mesher": { + "type": "staircase", + "options": { + "compress": true, + "exportGrid": true + } + } +} +``` + ## Contributing ## Citing this work diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index dbeccd0..6ce1de5 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -1,9 +1,11 @@ #include "launcher.h" #include "vtkIO.h" +#include "meshers/MesherBase.h" #include "meshers/StaircaseMesher.h" #include "meshers/ConformalMesher.h" #include "utils/GridTools.h" +#include "utils/MeshTools.h" #include #include @@ -13,6 +15,7 @@ #include #include #include +#include namespace meshlib::app { @@ -21,20 +24,20 @@ using namespace vtkIO; namespace po = boost::program_options; -Grid parseGridFromJSON(const nlohmann::json &j) +Grid parseGridFromJSON(const nlohmann::json &fileData) { - if (j.find("planes") != j.end()) { - return j["planes"]; + if (fileData.find("planes") != fileData.end()) { + return fileData["planes"]; } else { std::array nCells = { - j["numberOfCells"][0], - j["numberOfCells"][1], - j["numberOfCells"][2] + fileData["numberOfCells"][0], + fileData["numberOfCells"][1], + fileData["numberOfCells"][2] }; std::array min, max; - min = j["boundingBox"][0]; - max = j["boundingBox"][1]; + min = fileData["boundingBox"][0]; + max = fileData["boundingBox"][1]; return { utils::GridTools::linspace(min[0], max[0], nCells[0] + 1), @@ -44,48 +47,90 @@ Grid parseGridFromJSON(const nlohmann::json &j) } } -Mesh readMesh(const std::string &fn) +std::vector readObjectsFromJSON(const nlohmann::json& fileData) { - nlohmann::json j; - - { - std::ifstream i(fn); - i >> j; + std::vector objects; + + if (fileData.contains("objects")) { + for (const auto& obj : fileData["objects"]) { + ObjectDefinition objDef; + objDef.filename = obj["filename"].get(); + objDef.group = obj.value("group", std::filesystem::path(objDef.filename).stem().string()); + if (obj.contains("volume")){ + objDef.isVolume = obj["volume"]; + } + if (obj.contains("mesher")) { + objDef.mesherOverride = obj["mesher"]; + } + objects.push_back(objDef); + } + } else if (fileData.contains("object")) { + ObjectDefinition objDef; + objDef.filename = fileData["object"]["filename"].get(); + objDef.group = std::filesystem::path(objDef.filename).stem().string(); + if (fileData["object"].contains("volume")){ + objDef.isVolume = fileData["object"]["volume"]; + } + if (fileData.contains("mesher")) { + objDef.mesherOverride = fileData["mesher"]; + } + objects.push_back(objDef); + } else { + throw std::runtime_error("No objects defined in input file"); } - std::filesystem::path caseFolder = std::filesystem::path(fn).parent_path(); - std::filesystem::path objPathFromInput = j["object"]["filename"]; - std::filesystem::path meshObjectPath = caseFolder / objPathFromInput; + return objects; +} + +Mesh readMesh(const nlohmann::json& fileData, const std::filesystem::path& folderPath, const ObjectDefinition& objDef) +{ + std::filesystem::path meshObjectPath = folderPath / objDef.filename; std::cout << "-- Reading mesh groups from: " << meshObjectPath; Mesh res = vtkIO::readInputMesh(meshObjectPath); std::cout << "....... [OK]" << std::endl; std::cout << "-- Reading grid from input file"; - res.grid = parseGridFromJSON(j["grid"]); + res.grid = parseGridFromJSON(fileData["grid"]); std::cout << "....... [OK]" << std::endl; + if (res.groups.empty()) { + res.groups.push_back(Group{objDef.group, {}}); + } else { + auto auxResult = utils::meshTools::extractGroupsByName(res, {objDef.group}); + if (auxResult.countElems() != 0) { + return auxResult; + } else { + res.groups[0].name = objDef.group; + } + } + return res; } -std::string readMesherType(const std::string &fn) -{ - nlohmann::json j; +std::string readMesherType(const nlohmann::json& fileData, const std::optional& override) { - std::ifstream i(fn); - i >> j; + nlohmann::json mesherConfig; + + if (override.has_value()) { + mesherConfig = *override; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; + } else { + return meshlib::app::staircase_mesher; } - if (j["mesher"].contains("type")) { - return j["mesher"]["type"]; + + if (mesherConfig.contains("type")) { + return mesherConfig["type"]; } else { return meshlib::app::staircase_mesher; } } -std::string readExtension(const std::string &fn) +std::string readExtension(const nlohmann::json& fileData, const std::optional& override) { - auto mesherType = readMesherType(fn); + auto mesherType = readMesherType(fileData, override); if (mesherType == meshlib::app::staircase_mesher) { return "str"; } else if (mesherType == meshlib::app::conformal_mesher) { @@ -95,65 +140,71 @@ std::string readExtension(const std::string &fn) } } -meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const std::string &fn) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; +meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const nlohmann::json &fileData, bool isVolume, const std::optional& override) +{ + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } + meshlib::meshers::StaircaseMesherOptions res; - if (j["object"].contains("volume")) { - res.isVolume = j["object"]["volume"]; - } - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("compress")) { - res.compress = j["mesher"]["options"]["compress"]; + + res.isVolume = isVolume; + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("compress")) { + res.compress = mesherConfig["options"]["compress"]; } return res; } -meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string &fn) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; +meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const nlohmann::json& fileData, const std::optional& override) +{ + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } + meshlib::meshers::ConformalMesherOptions res; - if (j["object"].contains("volume")) { - res.isVolume = j["object"]["volume"]; - } - - - if (j["mesher"].contains("options")) { - res.snapperOptions.edgePoints = j["mesher"]["options"]["edgePoints"]; - res.snapperOptions.forbiddenLength = j["mesher"]["options"]["forbiddenLength"]; + if (mesherConfig.contains("options")) { + res.snapperOptions.edgePoints = mesherConfig["options"]["edgePoints"]; + res.snapperOptions.forbiddenLength = mesherConfig["options"]["forbiddenLength"]; } return res; } -bool readExportGridOption(const std::string &fn) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; +bool readExportGridOption(const nlohmann::json& fileData, const std::optional& override) +{ + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("exportGrid")) { - return j["mesher"]["options"]["exportGrid"]; + + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("exportGrid")) { + return mesherConfig["options"]["exportGrid"]; } return true; } -std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) + +std::unique_ptr buildMesher(const Mesh& in, const nlohmann::json & fileData, const ObjectDefinition& objDef) { - auto mesherType = readMesherType(fn); + auto mesherType = readMesherType(fileData, objDef.mesherOverride); + if (mesherType == meshlib::app::staircase_mesher) { - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn)}); + return std::make_unique(meshlib::meshers::StaircaseMesher{ + in, + 4, + readStaircaseMesherOptions(fileData, objDef.isVolume, objDef.mesherOverride) + }); } else if (mesherType == meshlib::app::conformal_mesher) { - return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); + return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fileData, objDef.mesherOverride)}); } else { throw std::runtime_error("Unsupported mesher type"); } @@ -176,27 +227,47 @@ int launcher(int argc, const char* argv[]) return EXIT_SUCCESS; } - // Input - std::string inputFilename = vm["input"].as(); - std::cout << "-- Input file is: " << inputFilename << std::endl; + std::string inputFileName = vm["input"].as(); + std::cout << "-- Input file is: " << inputFileName << std::endl; - Mesh mesh = readMesh(inputFilename); + nlohmann::json inputFileData; + { + std::ifstream i(inputFileName); + i >> inputFileData; + } + std::vector objects = readObjectsFromJSON(inputFileData); + std::filesystem::path outputFolder = getFolder(inputFileName); + auto basename = getBasename(inputFileName); - // Mesh - auto mesher = buildMesher(mesh, inputFilename); - Mesh resultMesh = mesher->mesh(); + Mesh firstMesh; + bool first = true; - std::filesystem::path outputFolder = getFolder(inputFilename); - auto basename = getBasename(inputFilename); - auto extension = readExtension(inputFilename); - - exportMeshToVTU(outputFolder / (basename + ".tessellator." + extension + ".vtk"), resultMesh); - if (readExportGridOption(inputFilename)) { - exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), resultMesh.grid); + for (const auto& objDef : objects) { + std::cout << "\n-- Processing object: " << objDef.filename << " (group: " << objDef.group << ")" << std::endl; + + Mesh mesh = readMesh(inputFileData, outputFolder, objDef); + + auto mesher = buildMesher(mesh, inputFileData, objDef); + Mesh resultMesh = mesher->mesh(); + + if (first) { + firstMesh = resultMesh; + first = false; + } + + auto extension = readExtension(inputFileData, objDef.mesherOverride); + std::string outputFileName = objDef.group + ".tessellator." + extension + ".vtk"; + exportMeshToVTU(outputFolder / outputFileName, resultMesh); + std::cout << "-- Exported: " << outputFileName << std::endl; + } + + if (!first && readExportGridOption(inputFileData, std::nullopt)) { + exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), firstMesh.grid); + std::cout << "-- Exported grid: " << basename << ".tessellator.grid.vtk" << std::endl; } return EXIT_SUCCESS; } -} \ No newline at end of file +} diff --git a/src/app/launcher.h b/src/app/launcher.h index d65d218..48823c3 100644 --- a/src/app/launcher.h +++ b/src/app/launcher.h @@ -1,7 +1,10 @@ #pragma once #include "types/Mesh.h" +#include "meshers/MesherBase.h" #include +#include +#include #include namespace meshlib::app { @@ -9,7 +12,17 @@ namespace meshlib::app { const std::string conformal_mesher ("conformal"); const std::string staircase_mesher ("staircase"); +struct ObjectDefinition { + std::string filename; + std::string group; + bool isVolume = false; + std::optional mesherOverride; +}; + int launcher(int argc, const char* argv[]); -Grid parseGridFromJSON(const nlohmann::json& j); +Grid parseGridFromJSON(const nlohmann::json& fileData); +std::vector readObjectsFromJSON(const nlohmann::json& fileData); +Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef); +std::unique_ptr buildMesher(const Mesh& in, const nlohmann::json& fileData, const ObjectDefinition& objDef); } \ No newline at end of file diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index f1e6c2e..d55a561 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -162,12 +163,26 @@ vtkSmartPointer toVTKGroupsArray(const Mesh& mesh) return groupsDataArray; } +vtkSmartPointer toVTKGroupNamesArray(const Mesh& mesh) +{ + vtkNew groupNamesArray; + groupNamesArray->SetName("groupNames"); + groupNamesArray->SetNumberOfComponents(1); + + for (const auto& group : mesh.groups) { + groupNamesArray->InsertNextValue(group.name.c_str()); + } + + return groupNamesArray; +} + vtkSmartPointer elementsToVTU(const Mesh& mesh) { vtkNew vtu; vtu->SetPoints(toVTKPoints(mesh.coordinates)); vtu->GetCellData()->AddArray(toVTKGroupsArray(mesh)); + vtu->GetCellData()->AddArray(toVTKGroupNamesArray(mesh)); std::vector cellTypes; cellTypes.reserve(mesh.countElems()); diff --git a/src/meshers/ConformalMesher.h b/src/meshers/ConformalMesher.h index 8fe5540..26a9858 100644 --- a/src/meshers/ConformalMesher.h +++ b/src/meshers/ConformalMesher.h @@ -17,6 +17,8 @@ class ConformalMesher : public MesherBase { virtual ~ConformalMesher() = default; Mesh mesh() const; + + const ConformalMesherOptions & getOptions() const { return opts_; } static std::set findNonConformalCells(const Mesh& mesh); static std::set cellsWithMoreThanAVertexInsideEdge(const Mesh& mesh); diff --git a/src/meshers/MesherBase.h b/src/meshers/MesherBase.h index 938f949..c0bac3e 100644 --- a/src/meshers/MesherBase.h +++ b/src/meshers/MesherBase.h @@ -11,6 +11,7 @@ class MesherBase { MesherBase(const Mesh& in); virtual ~MesherBase() = default; virtual Mesh mesh() const = 0; + const MesherBaseOptions & getOptions() const { return opts_; } protected: virtual void process(Mesh&) const = 0; diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index df0b9bb..3cabd4b 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -11,6 +11,7 @@ class StaircaseMesher : public MesherBase { StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions()); virtual ~StaircaseMesher() = default; Mesh mesh() const; + const StaircaseMesherOptions & getOptions() const { return opts_; } private: int decimalPlacesInCollapser_; diff --git a/src/types/Mesh.h b/src/types/Mesh.h index bb0d41a..dcccb37 100644 --- a/src/types/Mesh.h +++ b/src/types/Mesh.h @@ -134,10 +134,15 @@ typedef std::size_t ElementId; typedef std::vector Elements; struct Group { + std::string name; std::vector elements; + Group() = default; + Group(const std::vector& elems) : elements(elems) {} + Group(const std::string& n, const std::vector& elems) : name(n), elements(elems) {} + bool operator==(const Group& rhs) const { - return elements == rhs.elements; + return name == rhs.name && elements == rhs.elements; } std::map> buildCoordToElemMap() const { @@ -155,6 +160,7 @@ struct Group { friend class boost::serialization::access; template void serialize(Archive& ar, const unsigned int version) { + ar& name; ar& elements; } }; diff --git a/src/utils/MeshTools.cpp b/src/utils/MeshTools.cpp index b2a25f4..02bbe57 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -418,4 +418,54 @@ bool isAClosedTopology(const Elements& es) return CoordGraph(es).getBoundaryGraph().getVertices().size() == 0; } +Mesh extractGroupsByName(const Mesh& mesh, const std::vector& groupNames) +{ + Mesh result; + result.grid = mesh.grid; + + std::map coordRemap; + std::map> groupCoordIds; + + for (const auto& groupName : groupNames) { + auto it = std::find_if(mesh.groups.begin(), mesh.groups.end(), + [&groupName](const Group& g) { return g.name == groupName; }); + + if (it != mesh.groups.end()) { + result.groups.push_back(*it); + + for (const auto& elem : it->elements) { + for (const auto& vId : elem.vertices) { + std::string coordKey = groupName + "_" + std::to_string(vId); + if (coordRemap.find(coordKey) == coordRemap.end()) { + CoordinateId newVId = result.coordinates.size(); + result.coordinates.push_back(mesh.coordinates[vId]); + coordRemap[coordKey] = newVId; + groupCoordIds[groupName].push_back(vId); + } + } + } + } else { + result.groups.push_back(Group{groupName, {}}); + } + } + + for (auto& group : result.groups) { + std::string groupName = group.name; + std::map elemRemap; + + for (const auto& oldVId : groupCoordIds[groupName]) { + std::string coordKey = groupName + "_" + std::to_string(oldVId); + elemRemap[oldVId] = coordRemap[coordKey]; + } + + for (auto& elem : group.elements) { + for (auto& vId : elem.vertices) { + vId = elemRemap[vId]; + } + } + } + + return result; +} + } \ No newline at end of file diff --git a/src/utils/MeshTools.h b/src/utils/MeshTools.h index 688c0cb..5fc4546 100644 --- a/src/utils/MeshTools.h +++ b/src/utils/MeshTools.h @@ -50,4 +50,6 @@ void mergeMeshAsNewGroup(Mesh& lMesh, const Mesh& iMesh); bool isAClosedTopology(const Elements& es); +Mesh extractGroupsByName(const Mesh& mesh, const std::vector& groupNames); + } \ No newline at end of file diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 029b376..5b3f3e4 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -2,6 +2,8 @@ #include "app/launcher.h" #include "types/Mesh.h" +#include "meshers/StaircaseMesher.h" + #include #include @@ -15,7 +17,7 @@ TEST_F(LauncherTest, prints_help) { int ac = 2; const char* av[] = { NULL, "-h" }; - EXPECT_EQ(meshlib::app::launcher(ac, av), EXIT_SUCCESS); + EXPECT_EQ(launcher(ac, av), EXIT_SUCCESS); } TEST_F(LauncherTest, parse_rectilinear_grid) @@ -28,7 +30,7 @@ TEST_F(LauncherTest, parse_rectilinear_grid) i >> j; } - meshlib::Grid grid = meshlib::app::parseGridFromJSON(j["grid"]); + meshlib::Grid grid = parseGridFromJSON(j["grid"]); meshlib::Grid expectedGrid({ std::vector{600, 603.25}, @@ -49,32 +51,84 @@ TEST_F(LauncherTest, parse_rectilinear_grid) } } -TEST_F(LauncherTest, launches_alhambra_case) +TEST_F(LauncherTest, builds_staircased_mesher_default) { - int ac = 3; - const char* av[] = { NULL, "-i", "testData/cases/alhambra/alhambra.tessellator.json"}; - int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); - EXPECT_EQ(exitCode, EXIT_SUCCESS); + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + std::string fileName = "testData/cases/longPolyline/longPolyline_legacy.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + + ObjectDefinition objDef; + auto mesher = buildMesher(meshMock, j, objDef); + + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); + const auto & options = dynamic_cast(*mesher).getOptions(); + EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.compress, false); } -TEST_F(LauncherTest, parses_staircased_without_compression) +TEST_F(LauncherTest, builds_staircased_mesher_without_compression) { - int ac = 3; - const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline.tessellator.json" }; - int exitCode; + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + std::string fileName = "testData/cases/longPolyline/longPolyline.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + + ObjectDefinition objDef; + auto mesher = buildMesher(meshMock, j, objDef); + + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); + const auto & options = dynamic_cast(*mesher).getOptions(); + EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.compress, false); +} + +TEST_F(LauncherTest, builds_staircased_mesher_with_compression) +{ + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + std::string fileName = "testData/cases/longPolyline/longPolyline_compression.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + + ObjectDefinition objDef; + auto mesher = buildMesher(meshMock, j, objDef); - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); - EXPECT_EQ(exitCode, EXIT_SUCCESS); + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); + const auto & options = dynamic_cast(*mesher).getOptions(); + EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.compress, true); } -TEST_F(LauncherTest, parses_staircased_with_compression) +TEST_F(LauncherTest, launches_alhambra_case) { int ac = 3; - const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline_compression.tessellator.json" }; + const char* av[] = { NULL, "-i", "testData/cases/alhambra/alhambra.tessellator.json"}; int exitCode; - - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -83,7 +137,7 @@ TEST_F(LauncherTest, launches_conformal_alhambra_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/alhambra/alhambra.conformal.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -93,7 +147,7 @@ TEST_F(LauncherTest, launches_sphere_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/sphere/sphere.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -102,7 +156,7 @@ TEST_F(LauncherTest, launches_closed_sphere_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/sphere/closed_sphere.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -111,7 +165,7 @@ TEST_F(LauncherTest, launches_conformal_sphere_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/sphere/sphere.conformal.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -120,7 +174,7 @@ TEST_F(LauncherTest, launches_conformal_thinCylinder_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/thinCylinder/thinCylinder.conformal.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -129,7 +183,7 @@ TEST_F(LauncherTest, launches_thinCylinder_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/thinCylinder/thinCylinder.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -138,7 +192,7 @@ TEST_F(LauncherTest, launches_cone_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/cone/cone.tessellator.json" }; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -147,7 +201,7 @@ TEST_F(LauncherTest, launches_long_polyline_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline_legacy.tessellator.json" }; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -156,7 +210,148 @@ TEST_F(LauncherTest, launches_conformal_cone_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/cone/cone.conformal.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, readObjectsFromJSON_basic) +{ + std::string fileName = "testData/cases/multiObject/basic.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + auto objects = readObjectsFromJSON(j); + EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "sphere_group"); + EXPECT_TRUE(objects[0].isVolume); + EXPECT_FALSE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[1].filename, "cone.stl"); + EXPECT_EQ(objects[1].group, "cone_group"); + EXPECT_FALSE(objects[1].isVolume); + EXPECT_FALSE(objects[1].mesherOverride.has_value()); +} + +TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) +{ + std::string fileName = "testData/cases/multiObject/mixedMesher.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + auto objects = readObjectsFromJSON(j); + EXPECT_EQ(objects.size(), 3); + + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_FALSE(objects[0].isVolume); + EXPECT_TRUE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[0].mesherOverride.value()["type"], "staircase"); + EXPECT_TRUE(objects[0].mesherOverride.value().contains("options")); + EXPECT_TRUE(objects[0].mesherOverride.value()["options"].contains("compress")); + EXPECT_TRUE(objects[0].mesherOverride.value()["options"]["compress"]); + + EXPECT_EQ(objects[1].filename, "cone.stl"); + EXPECT_FALSE(objects[1].isVolume); + EXPECT_TRUE(objects[1].mesherOverride.has_value()); + EXPECT_EQ(objects[1].mesherOverride.value()["type"], "conformal"); + + EXPECT_EQ(objects[2].filename, "cone.stl"); + EXPECT_FALSE(objects[2].isVolume); + EXPECT_FALSE(objects[2].mesherOverride.has_value()); +} + +TEST_F(LauncherTest, readObjectsFromJSON_singleObject) +{ + std::string fileName = "testData/cases/multiObject/singleObject.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + auto objects = readObjectsFromJSON(j); + EXPECT_EQ(objects.size(), 1); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "default_group"); + EXPECT_FALSE(objects[0].isVolume); +} + +TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) +{ + std::string fileName = "testData/cases/sphere/closed_sphere.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + auto objects = readObjectsFromJSON(j); + EXPECT_EQ(objects.size(), 1); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "sphere"); + EXPECT_TRUE(objects[0].isVolume); +} + +TEST_F(LauncherTest, builds_staircased_mesher_with_override) +{ + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + + std::string fileName = "testData/cases/multiObject/mixedMesher.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + + auto objects = readObjectsFromJSON(j); + + auto mesher = meshlib::app::buildMesher(meshMock, j, objects[0]); + + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); + const auto & options = dynamic_cast(*mesher).getOptions(); + EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.compress, true); +} + +TEST_F(LauncherTest, launches_multiObject_basic) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/basic.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_mixedMesher) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/mixedMesher.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_singleObject) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/singleObject.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_sameFileMultipleGroups) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/sameFileMultipleGroups.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } diff --git a/test/utils/MeshToolsTest.cpp b/test/utils/MeshToolsTest.cpp index b46c30a..8078d16 100644 --- a/test/utils/MeshToolsTest.cpp +++ b/test/utils/MeshToolsTest.cpp @@ -720,4 +720,90 @@ TEST_F(MeshToolsTest, reduceGrid_epsilon_coord) } } +TEST_F(MeshToolsTest, extractGroupsByName_singleGroup) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a"}); + + EXPECT_EQ(result.groups.size(), 1); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.coordinates.size(), 3); +} + +TEST_F(MeshToolsTest, extractGroupsByName_multipleGroups) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}), + Coordinate({1.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}), + Group("group_c", {Element({2, 3}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "group_c"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.groups[1].name, "group_c"); + EXPECT_EQ(result.groups[1].elements.size(), 1); +} + +TEST_F(MeshToolsTest, extractGroupsByName_nonExistentGroup) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "non_existent"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.groups[1].name, "non_existent"); + EXPECT_EQ(result.groups[1].elements.size(), 0); +} + +TEST_F(MeshToolsTest, extractGroupsByName_coordinateRemapping) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "group_b"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].elements[0].vertices, std::vector({0, 1, 2})); + EXPECT_EQ(result.groups[1].elements[0].vertices, std::vector({3, 4})); + EXPECT_EQ(result.coordinates.size(), 5); +} + } \ No newline at end of file diff --git a/testData/cases/multiObject/basic.tessellator.json b/testData/cases/multiObject/basic.tessellator.json new file mode 100644 index 0000000..5724c92 --- /dev/null +++ b/testData/cases/multiObject/basic.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "volume": true, "group": "sphere_group"}, + {"filename": "cone.stl", "group": "cone_group"} + ] +} diff --git a/testData/cases/multiObject/cone.stl b/testData/cases/multiObject/cone.stl new file mode 100644 index 0000000..b855cac Binary files /dev/null and b/testData/cases/multiObject/cone.stl differ diff --git a/testData/cases/multiObject/mixedMesher.tessellator.json b/testData/cases/multiObject/mixedMesher.tessellator.json new file mode 100644 index 0000000..ca2afa7 --- /dev/null +++ b/testData/cases/multiObject/mixedMesher.tessellator.json @@ -0,0 +1,17 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "sphere_group", "mesher": {"type": "staircase", "options": { "compress" : true} } }, + {"filename": "cone.stl", "group": "cone_group", "mesher": {"type": "conformal"}}, + {"filename": "cone.stl", "group": "default_group"} + ] +} diff --git a/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json b/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json new file mode 100644 index 0000000..b1bcd39 --- /dev/null +++ b/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "group_a"}, + {"filename": "sphere.stl", "group": "group_b"} + ] +} diff --git a/testData/cases/multiObject/singleObject.tessellator.json b/testData/cases/multiObject/singleObject.tessellator.json new file mode 100644 index 0000000..9216505 --- /dev/null +++ b/testData/cases/multiObject/singleObject.tessellator.json @@ -0,0 +1,15 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "default_group"} + ] +} diff --git a/testData/cases/multiObject/sphere.stl b/testData/cases/multiObject/sphere.stl new file mode 100644 index 0000000..29885a1 Binary files /dev/null and b/testData/cases/multiObject/sphere.stl differ