From f60edbaa77559a19499834db76ac5ecf64a509b9 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Sun, 17 May 2026 20:09:40 +0200 Subject: [PATCH 1/7] AI assisted implementation of multiobject meshing --- src/app/launcher.cpp | 179 ++++++++++++++++++++++++++++------- src/app/launcher.h | 12 +++ src/app/vtkIO.cpp | 15 +++ src/types/Mesh.h | 8 +- src/utils/MeshTools.cpp | 54 +++++++++++ src/utils/MeshTools.h | 2 + test/app/launcherTest.cpp | 75 +++++++++++++++ test/utils/MeshToolsTest.cpp | 86 +++++++++++++++++ 8 files changed, 394 insertions(+), 37 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index dbeccd0..4d4174b 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -4,6 +4,7 @@ #include "meshers/StaircaseMesher.h" #include "meshers/ConformalMesher.h" #include "utils/GridTools.h" +#include "utils/MeshTools.h" #include #include @@ -13,6 +14,8 @@ #include #include #include +#include +#include namespace meshlib::app { @@ -44,18 +47,51 @@ Grid parseGridFromJSON(const nlohmann::json &j) } } -Mesh readMesh(const std::string &fn) +std::vector readObjectsFromJSON(const std::string& fn) { nlohmann::json j; + { + std::ifstream i(fn); + i >> j; + } + + std::vector objects; + + if (j.contains("objects")) { + for (const auto& obj : j["objects"]) { + ObjectDefinition objDef; + objDef.filename = obj["filename"]; + objDef.group = obj.value("group", std::filesystem::path(obj["filename"]).stem().string()); + if (obj.contains("mesher")) { + objDef.mesherOverride = obj["mesher"]; + } + objects.push_back(objDef); + } + } else if (j.contains("object")) { + ObjectDefinition objDef; + objDef.filename = j["object"]["filename"]; + objDef.group = std::filesystem::path(j["object"]["filename"]).stem().string(); + if (j.contains("mesher")) { + objDef.mesherOverride = j["mesher"]; + } + objects.push_back(objDef); + } else { + throw std::runtime_error("No objects defined in input file"); + } + return objects; +} + +Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) +{ + nlohmann::json j; { std::ifstream i(fn); i >> j; } std::filesystem::path caseFolder = std::filesystem::path(fn).parent_path(); - std::filesystem::path objPathFromInput = j["object"]["filename"]; - std::filesystem::path meshObjectPath = caseFolder / objPathFromInput; + std::filesystem::path meshObjectPath = caseFolder / objDef.filename; std::cout << "-- Reading mesh groups from: " << meshObjectPath; Mesh res = vtkIO::readInputMesh(meshObjectPath); @@ -65,27 +101,48 @@ Mesh readMesh(const std::string &fn) res.grid = parseGridFromJSON(j["grid"]); std::cout << "....... [OK]" << std::endl; + if (res.groups.empty()) { + res.groups.push_back(Group{objDef.group, {}}); + } else { + res = utils::meshTools::extractGroupsByName(res, {objDef.group}); + if (res.groups.empty()) { + res.groups.push_back(Group{objDef.group, {}}); + } else { + res.groups[0].name = objDef.group; + } + } + return res; } -std::string readMesherType(const std::string &fn) +std::string readMesherType(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } - if (j["mesher"].contains("type")) { - return j["mesher"]["type"]; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } else { + return meshlib::app::staircase_mesher; + } + + if (mesherConfig.contains("type")) { + return mesherConfig["type"]; } else { return meshlib::app::staircase_mesher; } } -std::string readExtension(const std::string &fn) +std::string readExtension(const std::string& fn, const std::optional& override) { - auto mesherType = readMesherType(fn); + auto mesherType = readMesherType(fn, override); if (mesherType == meshlib::app::staircase_mesher) { return "str"; } else if (mesherType == meshlib::app::conformal_mesher) { @@ -114,46 +171,82 @@ meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const std::s return res; } -meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string &fn) +meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + meshlib::meshers::ConformalMesherOptions res; - if (j["object"].contains("volume")) { - res.isVolume = j["object"]["volume"]; + if (mesherConfig.contains("options")) { + res.snapperOptions.edgePoints = mesherConfig["options"]["edgePoints"]; + res.snapperOptions.forbiddenLength = mesherConfig["options"]["forbiddenLength"]; } + return res; +} - - if (j["mesher"].contains("options")) { - res.snapperOptions.edgePoints = j["mesher"]["options"]["edgePoints"]; - res.snapperOptions.forbiddenLength = j["mesher"]["options"]["forbiddenLength"]; +bool readStaircaseMesherCompressOption(const std::string& fn, const std::optional& override) +{ + nlohmann::json j; + { + std::ifstream i(fn); + i >> j; } - return res; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("compress")) { + return mesherConfig["options"]["compress"]; + } + return false; } -bool readExportGridOption(const std::string &fn) +bool readExportGridOption(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("exportGrid")) { - return j["mesher"]["options"]["exportGrid"]; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + + 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 std::string& fn, const std::optional& override) { - auto mesherType = readMesherType(fn); + auto mesherType = readMesherType(fn, override); if (mesherType == meshlib::app::staircase_mesher) { - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn)}); + auto staircasedOptions = readStaircaseMesherOptions(fn); + staircasedOptions.compress = readStaircaseMesherCompressOption(fn, override); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, staircasedOptions}); } 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(fn, override)}); } else { throw std::runtime_error("Unsupported mesher type"); } @@ -176,24 +269,38 @@ 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; - Mesh mesh = readMesh(inputFilename); + std::vector objects = readObjectsFromJSON(inputFilename); + std::filesystem::path outputFolder = getFolder(inputFilename); + auto basename = getBasename(inputFilename); + Mesh firstMesh; + bool first = true; - // Mesh - auto mesher = buildMesher(mesh, inputFilename); - Mesh resultMesh = mesher->mesh(); + for (const auto& objDef : objects) { + std::cout << "\n-- Processing object: " << objDef.filename << " (group: " << objDef.group << ")" << std::endl; - 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); + Mesh mesh = readMesh(inputFilename, objDef); + + auto mesher = buildMesher(mesh, inputFilename, objDef.mesherOverride); + Mesh resultMesh = mesher->mesh(); + + if (first) { + firstMesh = resultMesh; + first = false; + } + + auto extension = readExtension(inputFilename, objDef.mesherOverride); + std::string outputFilename = objDef.group + ".tessellator." + extension + ".vtk"; + exportMeshToVTU(outputFolder / outputFilename, resultMesh); + std::cout << "-- Exported: " << outputFilename << std::endl; + } + + if (!first && readExportGridOption(inputFilename, std::nullopt)) { + exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), firstMesh.grid); + std::cout << "-- Exported grid: " << basename << ".tessellator.grid.vtk" << std::endl; } return EXIT_SUCCESS; diff --git a/src/app/launcher.h b/src/app/launcher.h index d65d218..bf65b34 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,16 @@ namespace meshlib::app { const std::string conformal_mesher ("conformal"); const std::string staircase_mesher ("staircase"); +struct ObjectDefinition { + std::string filename; + std::string group; + std::optional mesherOverride; +}; + int launcher(int argc, const char* argv[]); Grid parseGridFromJSON(const nlohmann::json& j); +std::vector readObjectsFromJSON(const std::string& fn); +Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef); +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const std::optional& override); } \ 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/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..d271f39 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -418,4 +418,58 @@ 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) { + groupCoordIds[groupName].clear(); + } + + 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..c459e88 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -160,3 +160,78 @@ TEST_F(LauncherTest, launches_conformal_cone_case) EXPECT_EQ(exitCode, EXIT_SUCCESS); } +TEST_F(LauncherTest, readObjectsFromJSON_basic) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/basic.tessellator.json"); + EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "sphere_group"); + 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].mesherOverride.has_value()); +} + +TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); + EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_FALSE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[1].filename, "cone.stl"); + EXPECT_TRUE(objects[1].mesherOverride.has_value()); + EXPECT_EQ(objects[1].mesherOverride.value()["type"], "conformal"); +} + +TEST_F(LauncherTest, readObjectsFromJSON_singleObject) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/singleObject.tessellator.json"); + EXPECT_EQ(objects.size(), 1); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "default_group"); +} + +TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) +{ + auto objects = readObjectsFromJSON("testData/cases/sphere/sphere.tessellator.json"); + EXPECT_EQ(objects.size(), 1); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "sphere"); +} + +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 = meshlib::app::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 = meshlib::app::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 = meshlib::app::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 = meshlib::app::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 From 4c607f11de1df9604ba13edd60fcb10d3414cdd8 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Mon, 18 May 2026 10:25:12 +0200 Subject: [PATCH 2/7] Document multi-object support, mesher selection, and options in README --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 7 deletions(-) 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 From f09b12b5f79beae690e79ac2988263654d0f8ff8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 11:20:12 +0000 Subject: [PATCH 3/7] Fix Windows JSON path conversion in launcher Agent-Logs-Url: https://github.com/OpenSEMBA/tessellator/sessions/9d4cb838-6891-4b15-a986-f5f0e798125b Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- src/app/launcher.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 4d4174b..d956a44 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -60,8 +60,8 @@ std::vector readObjectsFromJSON(const std::string& fn) if (j.contains("objects")) { for (const auto& obj : j["objects"]) { ObjectDefinition objDef; - objDef.filename = obj["filename"]; - objDef.group = obj.value("group", std::filesystem::path(obj["filename"]).stem().string()); + objDef.filename = obj["filename"].get(); + objDef.group = obj.value("group", std::filesystem::path(objDef.filename).stem().string()); if (obj.contains("mesher")) { objDef.mesherOverride = obj["mesher"]; } @@ -69,8 +69,8 @@ std::vector readObjectsFromJSON(const std::string& fn) } } else if (j.contains("object")) { ObjectDefinition objDef; - objDef.filename = j["object"]["filename"]; - objDef.group = std::filesystem::path(j["object"]["filename"]).stem().string(); + objDef.filename = j["object"]["filename"].get(); + objDef.group = std::filesystem::path(objDef.filename).stem().string(); if (j.contains("mesher")) { objDef.mesherOverride = j["mesher"]; } @@ -306,4 +306,4 @@ int launcher(int argc, const char* argv[]) return EXIT_SUCCESS; } -} \ No newline at end of file +} From 3f436b7fe5db50150f51c5c0ace319b993d2cf44 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Thu, 23 Jul 2026 12:56:12 +0000 Subject: [PATCH 4/7] Tessellator | Container | Debug in docker --- .vscode/settings.dev.json | 12 +++++++++++- CMakePresets.json | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) 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" From e9f5546a183e57de3dd9cbbf749bbf1cd14890ea Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Tue, 21 Jul 2026 15:29:24 +0000 Subject: [PATCH 5/7] Tessellator | Testing | Fix tests after rebase --- src/app/launcher.cpp | 34 ++++++++++-------- src/app/launcher.h | 3 +- test/app/launcherTest.cpp | 11 +++++- .../cases/multiObject/basic.tessellator.json | 16 +++++++++ testData/cases/multiObject/cone.stl | Bin 0 -> 205184 bytes .../multiObject/mixedMesher.tessellator.json | 16 +++++++++ .../sameFileMultipleGroups.tessellator.json | 16 +++++++++ .../multiObject/singleObject.tessellator.json | 15 ++++++++ testData/cases/multiObject/sphere.stl | Bin 0 -> 25984 bytes 9 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 testData/cases/multiObject/basic.tessellator.json create mode 100644 testData/cases/multiObject/cone.stl create mode 100644 testData/cases/multiObject/mixedMesher.tessellator.json create mode 100644 testData/cases/multiObject/sameFileMultipleGroups.tessellator.json create mode 100644 testData/cases/multiObject/singleObject.tessellator.json create mode 100644 testData/cases/multiObject/sphere.stl diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index d956a44..68dd532 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -1,6 +1,7 @@ #include "launcher.h" #include "vtkIO.h" +#include "meshers/MesherBase.h" #include "meshers/StaircaseMesher.h" #include "meshers/ConformalMesher.h" #include "utils/GridTools.h" @@ -14,7 +15,6 @@ #include #include #include -#include #include namespace meshlib::app { @@ -62,6 +62,9 @@ std::vector readObjectsFromJSON(const std::string& fn) 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"]; } @@ -71,6 +74,9 @@ std::vector readObjectsFromJSON(const std::string& fn) ObjectDefinition objDef; objDef.filename = j["object"]["filename"].get(); objDef.group = std::filesystem::path(objDef.filename).stem().string(); + if (j["object"].contains("volume")){ + objDef.isVolume = j["object"]["volume"]; + } if (j.contains("mesher")) { objDef.mesherOverride = j["mesher"]; } @@ -104,9 +110,9 @@ Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) if (res.groups.empty()) { res.groups.push_back(Group{objDef.group, {}}); } else { - res = utils::meshTools::extractGroupsByName(res, {objDef.group}); - if (res.groups.empty()) { - res.groups.push_back(Group{objDef.group, {}}); + auto auxResult = utils::meshTools::extractGroupsByName(res, {objDef.group}); + if (auxResult.countElems() != 0) { + return auxResult; } else { res.groups[0].name = objDef.group; } @@ -152,17 +158,17 @@ std::string readExtension(const std::string& fn, const std::optional> j; } + meshlib::meshers::StaircaseMesherOptions res; - if (j["object"].contains("volume")) { - res.isVolume = j["object"]["volume"]; - } + + res.isVolume = isVolume; if (j["mesher"].contains("options") && j["mesher"]["options"].contains("compress")) { res.compress = j["mesher"]["options"]["compress"]; @@ -238,15 +244,15 @@ bool readExportGridOption(const std::string& fn, const std::optional buildMesher(const Mesh& in, const std::string& fn, const std::optional& override) +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const ObjectDefinition& objDef) { - auto mesherType = readMesherType(fn, override); + auto mesherType = readMesherType(fn, objDef.mesherOverride); if (mesherType == meshlib::app::staircase_mesher) { - auto staircasedOptions = readStaircaseMesherOptions(fn); - staircasedOptions.compress = readStaircaseMesherCompressOption(fn, override); + auto staircasedOptions = readStaircaseMesherOptions(fn, objDef.isVolume); + staircasedOptions.compress = readStaircaseMesherCompressOption(fn, objDef.mesherOverride); return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, staircasedOptions}); } else if (mesherType == meshlib::app::conformal_mesher) { - return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, override)}); + return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, objDef.mesherOverride)}); } else { throw std::runtime_error("Unsupported mesher type"); } @@ -284,7 +290,7 @@ int launcher(int argc, const char* argv[]) Mesh mesh = readMesh(inputFilename, objDef); - auto mesher = buildMesher(mesh, inputFilename, objDef.mesherOverride); + auto mesher = buildMesher(mesh, inputFilename, objDef); Mesh resultMesh = mesher->mesh(); if (first) { diff --git a/src/app/launcher.h b/src/app/launcher.h index bf65b34..2fd828e 100644 --- a/src/app/launcher.h +++ b/src/app/launcher.h @@ -15,6 +15,7 @@ const std::string staircase_mesher ("staircase"); struct ObjectDefinition { std::string filename; std::string group; + bool isVolume = false; std::optional mesherOverride; }; @@ -22,6 +23,6 @@ int launcher(int argc, const char* argv[]); Grid parseGridFromJSON(const nlohmann::json& j); std::vector readObjectsFromJSON(const std::string& fn); Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef); -std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const std::optional& override); +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const ObjectDefinition& objDef); } \ No newline at end of file diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index c459e88..5c3ef6f 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -61,6 +61,8 @@ TEST_F(LauncherTest, launches_alhambra_case) TEST_F(LauncherTest, parses_staircased_without_compression) { int ac = 3; + // ObjectDefinition definition{"longPolyline.vtu", "Cable"}; + // auto mesh = meshlib::app::readMesh("testData/cases/longPolyline/longPolyline.tessellator.json", definition); const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline.tessellator.json" }; int exitCode; @@ -166,9 +168,11 @@ TEST_F(LauncherTest, readObjectsFromJSON_basic) 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()); } @@ -177,8 +181,11 @@ TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); EXPECT_EQ(objects.size(), 2); EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_FALSE(objects[0].isVolume); EXPECT_FALSE(objects[0].mesherOverride.has_value()); + 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"); } @@ -189,14 +196,16 @@ TEST_F(LauncherTest, readObjectsFromJSON_singleObject) 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) { - auto objects = readObjectsFromJSON("testData/cases/sphere/sphere.tessellator.json"); + auto objects = readObjectsFromJSON("testData/cases/sphere/closed_sphere.tessellator.json"); 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, launches_multiObject_basic) 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 0000000000000000000000000000000000000000..b855cac399afee6aaee522a1e8efc93cd5dc32e4 GIT binary patch literal 205184 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

Qj6w?%hYIL1}#FE*+Sdky5ss+vBd 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 UQr+V#@vZW|gA-QRf*%h5AC+uZBLDyZ literal 0 HcmV?d00001 diff --git a/testData/cases/multiObject/mixedMesher.tessellator.json b/testData/cases/multiObject/mixedMesher.tessellator.json new file mode 100644 index 0000000..23c3d55 --- /dev/null +++ b/testData/cases/multiObject/mixedMesher.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": "sphere_group"}, + {"filename": "cone.stl", "group": "cone_group", "mesher": {"type": "conformal"}} + ] +} 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 0000000000000000000000000000000000000000..29885a13380c6e8b354e081e448be9bd5d23a7ee GIT binary patch literal 25984 zcmb`Qd%RuKwa15O;?X9p&`LxlUKJr9n76ktVnGs8HE!oqdAD2X&K>TqGj2TF(mA-s^ei9BZz*zoW;W_kKS2_FSF6z1EuJ z_ZxG}F~^ujjX2_ zQF6o$Th!J+adGkDTeWQRr}wn^-{te{Gd`K|W^Mf7pDE(JxdXDRf7>nd2%;@w!%mOZ zZr$xkMeOzb`)#KU+*J`oTg2q!o=iq}JEr!+$|=S1H+E#>R;_OH@6PYw8;b1N`|)IH z`|{eWFP~-*9K|;j8C|=ecK9`a*S9)$;Wce@`wrK);v3qx+WL!olWDL2Fj?oGG4l2K z*~Aw|Xa24DyZk1RBOZMux##iik{8`~5W#OETBPk4uMC^s`)kQMXO9tEjZbIKK2+a| zZ)o3Y^I5kfADuNV@pJXqj9J-pgJ$bn@p<;G&bZ}@WbvohC9k>fAcCVf3nJ+s`lZ`X z9#q@w-Ghty+kB(GWv^S>UY*jj)BSIK@R$AC{ok~W$ckqVPJ4cId~Nr+{S3m*SN-H( zX}cTUwCJ%z`=oD-zpr*$^ALmJ?{eNmZvNqxi+}X19*O7fg6RW0E_m!F;C~}{vP{gkKOQq&J}0De)*uzy^LL2& zoCW*k+w{A=w*Nl4e#bLQ=C$=(zC^#A@6LG>x&O58$@71BT<3}i&H~XQAD{Go^3H~4 zGR1xQrITLCesk%A`sMsu&a}vaS2jt%*#F+7-F-O`oHwFHI)8O*Rb`QQJiU!3-@~< zd3%db6VKi4w!J(1p!Z+(%lWmOH<20Nd?`8L)W0S7xi2Szvp}@ShLOt~=hf%ymtXkS z_nQX|TCJX%vtYlxyNpVIKY4v^$)n#XhAsPE{lRyJW?pyHAUP&-$n3WC!xcv-o&VL} zAUKM)5oz|H*I4}ZCFJkZD)7+Gqv>S_4g!xcE^9dWqs)959#c3rtMr^yKYe0cg6EMyF_pl?Ju&+s3G~{ z*M6_u-D1JX%{3G5*4gE}*|~api?R9ji*8rnP6S7hF(TQdjgq1N*)RQ#o86(OZqs(b z);)E0Id68Z-g|17+P6maO}&RBf}?1E_Z@e4&pS5E9_Ia(i(Z;f-!f&Oj~O`Ac6P75 zY}53DfxXiw91|}+``zq--9FdZ<^0h8B3oTCAYDJFTl%E42N9fUqMfJy->dQqZn|D) zcgjw;HcvV8BAs2%o1Lp0ADWZ5?>$vL0}&iW#)!PI`Dy8igQnMpUArWIYJ6IseD;bq zKLhUT>*w|Pal8MUdK5Wh=*8(9*I!dhj_qy`F2bw3IcmS{hBxjfT4c?IlhUiM`l6QY zwcH^1yJV%vlXC|*{y1+tRn)*e-t4&Ryv^0-w5GM-v~50VY;?|dR8d556j>Q_>HYTAIT6q!B$>7pRsA`SDXU|%xHFQ|EaDx zv;lc6^3LZ+=7+ERcIww!B4|ybqfc!3_$qv%T9DSX7Ch&sYm%j()YTp*eEf}Us~dM! z3(_{$f=P3Ss+2sz4Hh&O9((;#5lnN0DREf(=-( z!}%)X8OSk_$@?tL#r==p;{*#Tg4VP)e>m@vu6rX%y$0WY{?+V;mEW!V5wsdvDRR{G z9n%AP?~$%?8YF_&BwA$d@BW$p_JL&@!`(1{L-Wm#XR8Hi8*9P72X9==Sa^rVi9~P| zSt;_nUC&MX44;)8)n{OS{Ecn(ajTAY>)@PrdIoY#F@S<_qbwma z$b7eBIdx6a_P~+p8;&JJkTFDyeDj}M7LAoVtIm%cylH1~dmm*9?Mxnv%wN4lF>k`f z$}1ws7@|e)Tyk_eYwwNIv5qApUmsMT_~P@*5;B847TLWqIsJ3bXOanyB}9-hM8`d+ zvLwXn%90RS>fe?nr=FbWhy8Jgt_C+fv9$ThWj&Q8w7<3aN$qdu>G&Nr>LY@qs4kHZ zk*Uptij*a^GkGksiCfR#+IV#CeF+g{4ACNIeN<1ry!zDiRcG@<{(gAdwu{@8CA7a~ z$?E=>)~?%jayrcMiU^LPR$N5Z-nZ}`&adZwZ}Y@E19tWQNoJ7wB9~nG-D0zS7S?>+ zP6Qc4v|FPVj!3($9FU&xZ2s7cP3zAM>h0_?r=9knx}jUAOPwz{{kR^Tm(P7!BYoQ6+Wdp<`xPfo`AqjwL~s%%3ZS3ayRVm za=&sC!BNzs$j_I~Xgv7&8R;vIyI&6ZZu85XYRX-*(lT+;E=m5z6BE**j=Mx~6crqq z*zimY>}z^Pl9iUb!6)W^e@+BPQI8_?j{95kogrh@!`0Dwows$nM!8G&QI9U~ z^W67~gEw8B_+2Uy(Q4rzbftN(4tykMV3m``6(A$WEE7KoE6xRuyF_pl^%(PsxsUonZb5lP zR$5+#Y^_EB22q{}bWyO#V>@kLobvGS^uL`?-0;fAolo!4Q<+GPSzg^bt$(p_$sk?N ziQp(IIPUJ$!-aK8nMf{CU2!*|+$DmnBwD2BfNN`8B$uR@I415f_SlYlW*w+ZB*!eT zj$C_nWB3Qt(n*eqL~s=K=(5h`*_|>moDb=Yl9gY{#0Hqy;h9MGQIBqS_uioz`}o~N zV4@<(N}^l7{X)kp`0$Keq8>#M$s|OOl|+k#6BFfC$ZRSTxf)Q9BH^S#y(kf6CD9^7 zSL|1`4cVN!EGkqC~Wf=zZXK@KTnB*T@V zm5qAshKCJ;qp0~ZGcn??8x`K~hFD$gO&(jehx}E6{FNd&ikcUhIAL^t_w=8p&gP8; zAI)?p4a1b}oa$AUFOcSod}Mimx!6v!e>%L{z7A2GT(AHWX}s9?-IdL^b#W1PkTH6 za`^akrepgS3*K}mKR&}Ur=2{u+`a2gmo+%_(DY2lb|N^6ekInIEJl4v;j@EexXYQv zT9a5`vKaLxim;hdK9!BO-?<$2WO^S0CVBjk-$QPhgnM>vBla0aOej-n?L8UM!c;@#u>>uNA% zr+%HMocWpCtIcVrCg_PAuRa)Dj8BGYd_@G+MYO4UNpWUgchO5#CsXITYbt9D!mS}y z#g;+4{FOV^f5@Hcy9~WS@OSC$L_&t8@GDM;SUde1MXgvxg*;s0S2ZFyir!A-3qub1b*B&HiZg10o=BuR&2Z;Y20=X%9Z&TOpOXn^ zX{ryZ%jzT4Oz0X#1V_<#i8K};Q#^Fa*V3tOjoSC@A)O<~+@Xr1R;;4_e*2_i)jm%r zC%ZL@2#%ufG8HK)>Llt^mt{NZG7N&F=(|Khp1SbsN60OxKFEB__Hdd}_%kmeIEubY z1hHs>6*(jGtv;#8st;;{o+!qNs=>evMNp4Kn{%85Irfa|vWhx0iM+8UF6U7=Y z%@l>20*&se3Hp^-x1_5Y5mXn^BCB`5u=v)snAD|g9PdI^!sU47z+Q~#Fx^@9lVm}r+n zUUi8^mM#LQnCMo>IqhVnh3N9j`?yu@3K#oe^~%U_`jt4liOXy!L{KY4n>zh8SWR=H)QZ)IuU7xO ztX4M&j-n?L31_JV&Qg`_WH|kbtHp7rWGfeqD|{7F;Hx62E}~sr;!eqOcS^Re+ZzNG zsXnL`%XWYMG0B~Otah;v5gbKN6nCi&^nY~tj66Jp-(6IDT=&mI|3?G;ABx~8>{uPI zLl=ZVg*e zb*q9d4J&|ZkSe0@5~;Zw$WO}}NQ0n)iH>!zs;IEuxi?wXq`IsI!^){UdLlTA-p<6L z30CBc>arR{1W+S_qv*Rtrk7EMuXIX7G@%-#is-w_Nan4w$HXA0V4~f3l<}3ua3Mlc zMNxP3L{imE6h2Q)1ocRC8TF;*IZhIyyUc5YDxxQfk)=OTGzjXE=$L!;y?rUV+Um%? zs_sLdpZgpAg9d?o>%XD>e(=b$@6I5g^E&-wjHwG`ot@5NR;`QgigA14Pc1{Vtq7{i zYVhq=g=rf00IDde%POj;t4}<=tWPuuj-r2bk!88Z(X4aW%c!EL33{Tqk5Ua9L>XCj zLB~}s$ExO6{fwHRUx^y@5puPvDdH>CrO`mS=X}@o%D6}-4SP#f6jemOA~LPjyJQg5 zBhhiUU7%`G?MxNX6N&iU_7_*5s=K1F!&U^rY zs6kMVM8~s)+pL~s;+SIl^&F5{JkoK@ymW@>_-$W;}$>URx-dL&vTL4OTr^ejOx&qv-9NKFS<&;d01TwYn;b>arRPIphKr@rvLm zMh0ar{L=DtDZ%+rHCLJUUGzjT7w*q%4T1``s_pA~sxK;gs?(5(_rF=E9<8Fj?sEDY z%6?~q;3)d8c&e{D=QF5tJ}=f96u#CVoI$7tsV=LiP+3!;vPKacMQhxuf%SSVy)9hICoS;I3-c$ zeY7o?l)dy}uNW-bFvYkvMLq&ex>R2}jGMs+JRgky3 z`wfD>OLRO9FHoPT+$CS>S7POv#&F>TToKd?(d8NakIK5&G@QjN+sRjY30Ha6s`54n zYK7?5%83&b9^sbJSV;5sM8bxwM=6G5#I z9ec@gbOS1P$z!gfvFas9$DtxPitA&Huaxa!-o1({->wX&mx#XIpL166l_GG?Y0x6~ zwQdkdLnV;1oqVO{W8JHIY9go)>c&+^x9(>!&)_INFV^oCzJ52QFN7nzMI@2IEo66ofZvrNOySO&PdNk1XZ_m?B-~oLs}7b z6uUV>hjarS(u$xa_`FyHnY;7Bs;62Jp(;$(MRd#o_&rXwqgQ=UMf59i$Kp>$4T5?k zTBJwoE{il&lB%MpBKnos)8OxuF$gM(N)zcHZVGWdCI-P#e4bRD7KQJ$2sN~-C~C#( zBUIxS=qpkLN7384PF7dnes)>kp8CvV=#Q#XT~<-3DmDm?qVIBNkY$aR#y(iRsv4Fq zULw}8_^7X{BU42o>NDuF%6oz98TI-I)!eEmYJ!?CE6L9(`-BaGdL%mLmVCym%I~^& zUzUZ{Qdw!47&5{IGQx`BD0&GOQ}=&i@vQQWlr+@iD-+2|%fu($O(Bi)rVxYRDEgJy zOXlzQF$i*(&x;*yh3{|+JqGS;=d_cRmRF&3QhhrS97WAXCi?pJ%2WH3AD@wmthBre zoff|0*C05Gnr}s9zCIDFOh#66y)vs>RU@oSWZ=($IaR zOeB}66_Fjn4K%J#*dkhYNY#mUd7svOO9nwEQU$SZJ4fHPGLc*g_4K-~#6GXw^?6l& z+lnA7iH@D#IXb-+VSktFpzl6&QMj*^tzv5BF4;#f5#thH-)<17P_LpI_v~d^Yc)V` zhBA@tqaI_mdNIzi9VmTpBoqiM^V8dOIx?~q@jmXxl0~f?)p1x&M)t*F$j*Lf@62Tzc{w4+m0lV;(=|^`JCn!et}uh3&B-^Bo7~Mt^UIr!41%NhyqH7w=O0x@SnW(^P%AOx z<+BtNxs^^P6it0yb(zE=R}ZKe4fh`l_zoPOG1{T(??5W2ALmE8hq_wb#|wSu!B?^ z5N-B@Rj136_S~Nv zRNWkkz`21zo7;L)zmpHQz17Jjsw>_V=I@6y2=a=2b2SsKTc8Yrqxig7m*MOBswxZR z71>91xf6GH?);OP2PzXoK3WmvG0`F+`{AqZtL%qbjW!^UMP7HeK%IN@YwCT%8m@@o zYrCM!T)6AU@wxD-Urp_SY_{=sWj@h$xcOX$LD0@*WqD(k^9;T&yz&gb53;hLuA*dR zw1>ab$soSM{)&z^_xHmY1Z_@qtX0z$C!CC_J!mzuGFI03TD7XWO%b#v(J|xY`%85W&gA~?H5n+nsa57O6*n*K`v|A+g@>!ym;And4EFX|AX*Rp|L zOMM1yVEryCQp_`Gf1+azr0yodxvb78twyEAiY<*3iJ&!!j^_sIeQ*OsorWrRqv*PgkT&}OIPSDvUf-_2M#`9X`6%qVhqFpYd zbpuUR@6*K|{3Vf4A*62=svZ=x*b+<@W z38*m@Jsh7c5^AG#uJ{a&;`3sSu)m+7s^9guEf_BOopJcLhq!+pYJ~kA5C*|fd_%Xp pD{F*xZ-7qEsxCN0_vCK)cZ{fSCxXAr?~7HFzRT3S6-U|G{XfD|Y0Lls literal 0 HcmV?d00001 From 9e2f363480df9c2a5d3705841f40edaad67893ab Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Wed, 22 Jul 2026 16:16:54 +0000 Subject: [PATCH 6/7] Tessellator | App | Upgrade structured mesher options for multi-object override --- src/app/launcher.cpp | 44 +++----- src/meshers/ConformalMesher.h | 2 + src/meshers/MesherBase.h | 1 + src/meshers/StaircaseMesher.h | 1 + test/app/launcherTest.cpp | 100 ++++++++++++++---- .../multiObject/mixedMesher.tessellator.json | 5 +- 6 files changed, 103 insertions(+), 50 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 68dd532..16552ce 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -158,20 +158,28 @@ std::string readExtension(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } - meshlib::meshers::StaircaseMesherOptions res; + meshlib::meshers::StaircaseMesherOptions res; + res.isVolume = isVolume; - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("compress")) { - res.compress = j["mesher"]["options"]["compress"]; + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("compress")) { + res.compress = mesherConfig["options"]["compress"]; } return res; @@ -200,28 +208,6 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::s return res; } -bool readStaircaseMesherCompressOption(const std::string& fn, const std::optional& override) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - - nlohmann::json mesherConfig; - if (override.has_value()) { - mesherConfig = *override; - } else if (j.contains("mesher")) { - mesherConfig = j["mesher"]; - } - - if (mesherConfig.contains("options") && - mesherConfig["options"].contains("compress")) { - return mesherConfig["options"]["compress"]; - } - return false; -} - bool readExportGridOption(const std::string& fn, const std::optional& override) { nlohmann::json j; @@ -248,9 +234,7 @@ std::unique_ptr buildMesher(const Mesh& in, const { auto mesherType = readMesherType(fn, objDef.mesherOverride); if (mesherType == meshlib::app::staircase_mesher) { - auto staircasedOptions = readStaircaseMesherOptions(fn, objDef.isVolume); - staircasedOptions.compress = readStaircaseMesherCompressOption(fn, objDef.mesherOverride); - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, staircasedOptions}); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn, objDef.isVolume, objDef.mesherOverride)}); } else if (mesherType == meshlib::app::conformal_mesher) { return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, objDef.mesherOverride)}); } else { 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/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 5c3ef6f..536e8f0 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 @@ -58,26 +60,58 @@ TEST_F(LauncherTest, launches_alhambra_case) EXPECT_EQ(exitCode, EXIT_SUCCESS); } -TEST_F(LauncherTest, parses_staircased_without_compression) +TEST_F(LauncherTest, builds_staircased_mesher_default) { - int ac = 3; - // ObjectDefinition definition{"longPolyline.vtu", "Cable"}; - // auto mesh = meshlib::app::readMesh("testData/cases/longPolyline/longPolyline.tessellator.json", definition); - const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline.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} + }; + + ObjectDefinition objDef; + auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline_legacy.tessellator.json", 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_with_compression) +TEST_F(LauncherTest, builds_staircased_mesher_without_compression) { - int ac = 3; - const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline_compression.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} + }; + + ObjectDefinition objDef; + auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline.tessellator.json", 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} + }; + + ObjectDefinition objDef; + auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline_compression.tessellator.json", 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, true); } TEST_F(LauncherTest, launches_conformal_alhambra_case) @@ -179,15 +213,24 @@ TEST_F(LauncherTest, readObjectsFromJSON_basic) TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) { auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); - EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects.size(), 3); + EXPECT_EQ(objects[0].filename, "sphere.stl"); EXPECT_FALSE(objects[0].isVolume); - EXPECT_FALSE(objects[0].mesherOverride.has_value()); + 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) @@ -208,6 +251,27 @@ TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) 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"; + + auto objects = readObjectsFromJSON(filename); + + auto mesher = meshlib::app::buildMesher(meshMock, filename, 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; diff --git a/testData/cases/multiObject/mixedMesher.tessellator.json b/testData/cases/multiObject/mixedMesher.tessellator.json index 23c3d55..ca2afa7 100644 --- a/testData/cases/multiObject/mixedMesher.tessellator.json +++ b/testData/cases/multiObject/mixedMesher.tessellator.json @@ -10,7 +10,8 @@ "type": "staircase" }, "objects": [ - {"filename": "sphere.stl", "group": "sphere_group"}, - {"filename": "cone.stl", "group": "cone_group", "mesher": {"type": "conformal"}} + {"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"} ] } From d6f9303f5f4bba9539804723ed784262c3bd8cc6 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Thu, 23 Jul 2026 09:24:12 +0000 Subject: [PATCH 7/7] Tessellator | App | Apply PR requested changes --- src/app/launcher.cpp | 156 ++++++++++++++++---------------------- src/app/launcher.h | 6 +- src/utils/MeshTools.cpp | 4 - test/app/launcherTest.cpp | 117 +++++++++++++++++++--------- 4 files changed, 150 insertions(+), 133 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 16552ce..6ce1de5 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -24,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), @@ -47,18 +47,12 @@ Grid parseGridFromJSON(const nlohmann::json &j) } } -std::vector readObjectsFromJSON(const std::string& fn) +std::vector readObjectsFromJSON(const nlohmann::json& fileData) { - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - std::vector objects; - if (j.contains("objects")) { - for (const auto& obj : j["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()); @@ -70,15 +64,15 @@ std::vector readObjectsFromJSON(const std::string& fn) } objects.push_back(objDef); } - } else if (j.contains("object")) { + } else if (fileData.contains("object")) { ObjectDefinition objDef; - objDef.filename = j["object"]["filename"].get(); + objDef.filename = fileData["object"]["filename"].get(); objDef.group = std::filesystem::path(objDef.filename).stem().string(); - if (j["object"].contains("volume")){ - objDef.isVolume = j["object"]["volume"]; + if (fileData["object"].contains("volume")){ + objDef.isVolume = fileData["object"]["volume"]; } - if (j.contains("mesher")) { - objDef.mesherOverride = j["mesher"]; + if (fileData.contains("mesher")) { + objDef.mesherOverride = fileData["mesher"]; } objects.push_back(objDef); } else { @@ -88,23 +82,16 @@ std::vector readObjectsFromJSON(const std::string& fn) return objects; } -Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) +Mesh readMesh(const nlohmann::json& fileData, const std::filesystem::path& folderPath, const ObjectDefinition& objDef) { - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - - std::filesystem::path caseFolder = std::filesystem::path(fn).parent_path(); - std::filesystem::path meshObjectPath = caseFolder / objDef.filename; + 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()) { @@ -122,19 +109,14 @@ Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) } -std::string readMesherType(const std::string& fn, const std::optional& override) -{ - 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 (j.contains("mesher")) { - mesherConfig = j["mesher"]; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } else { return meshlib::app::staircase_mesher; } @@ -146,9 +128,9 @@ std::string readMesherType(const std::string& fn, const std::optional& override) +std::string readExtension(const nlohmann::json& fileData, const std::optional& override) { - auto mesherType = readMesherType(fn, override); + auto mesherType = readMesherType(fileData, override); if (mesherType == meshlib::app::staircase_mesher) { return "str"; } else if (mesherType == meshlib::app::conformal_mesher) { @@ -158,22 +140,15 @@ std::string readExtension(const std::string& fn, const std::optional& override) -{ - 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 (j.contains("mesher")) { - mesherConfig = j["mesher"]; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } - meshlib::meshers::StaircaseMesherOptions res; res.isVolume = isVolume; @@ -185,19 +160,13 @@ meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const std::s return res; } -meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string& fn, const std::optional& override) -{ - 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 (j.contains("mesher")) { - mesherConfig = j["mesher"]; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } meshlib::meshers::ConformalMesherOptions res; @@ -208,19 +177,13 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::s return res; } -bool readExportGridOption(const std::string& fn, const std::optional& override) -{ - 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 (j.contains("mesher")) { - mesherConfig = j["mesher"]; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } if (mesherConfig.contains("options") && @@ -230,13 +193,18 @@ bool readExportGridOption(const std::string& fn, const std::optional buildMesher(const Mesh& in, const std::string& fn, const ObjectDefinition& objDef) +std::unique_ptr buildMesher(const Mesh& in, const nlohmann::json & fileData, const ObjectDefinition& objDef) { - auto mesherType = readMesherType(fn, objDef.mesherOverride); + auto mesherType = readMesherType(fileData, objDef.mesherOverride); + if (mesherType == meshlib::app::staircase_mesher) { - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn, objDef.isVolume, objDef.mesherOverride)}); + 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, objDef.mesherOverride)}); + return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fileData, objDef.mesherOverride)}); } else { throw std::runtime_error("Unsupported mesher type"); } @@ -259,12 +227,18 @@ int launcher(int argc, const char* argv[]) return EXIT_SUCCESS; } - 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; + + nlohmann::json inputFileData; + { + std::ifstream i(inputFileName); + i >> inputFileData; + } - std::vector objects = readObjectsFromJSON(inputFilename); - std::filesystem::path outputFolder = getFolder(inputFilename); - auto basename = getBasename(inputFilename); + std::vector objects = readObjectsFromJSON(inputFileData); + std::filesystem::path outputFolder = getFolder(inputFileName); + auto basename = getBasename(inputFileName); Mesh firstMesh; bool first = true; @@ -272,9 +246,9 @@ int launcher(int argc, const char* argv[]) for (const auto& objDef : objects) { std::cout << "\n-- Processing object: " << objDef.filename << " (group: " << objDef.group << ")" << std::endl; - Mesh mesh = readMesh(inputFilename, objDef); + Mesh mesh = readMesh(inputFileData, outputFolder, objDef); - auto mesher = buildMesher(mesh, inputFilename, objDef); + auto mesher = buildMesher(mesh, inputFileData, objDef); Mesh resultMesh = mesher->mesh(); if (first) { @@ -282,13 +256,13 @@ int launcher(int argc, const char* argv[]) first = false; } - auto extension = readExtension(inputFilename, objDef.mesherOverride); - std::string outputFilename = objDef.group + ".tessellator." + extension + ".vtk"; - exportMeshToVTU(outputFolder / outputFilename, resultMesh); - std::cout << "-- Exported: " << outputFilename << std::endl; + 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(inputFilename, std::nullopt)) { + if (!first && readExportGridOption(inputFileData, std::nullopt)) { exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), firstMesh.grid); std::cout << "-- Exported grid: " << basename << ".tessellator.grid.vtk" << std::endl; } diff --git a/src/app/launcher.h b/src/app/launcher.h index 2fd828e..48823c3 100644 --- a/src/app/launcher.h +++ b/src/app/launcher.h @@ -20,9 +20,9 @@ struct ObjectDefinition { }; int launcher(int argc, const char* argv[]); -Grid parseGridFromJSON(const nlohmann::json& j); -std::vector readObjectsFromJSON(const std::string& fn); +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 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/utils/MeshTools.cpp b/src/utils/MeshTools.cpp index d271f39..02bbe57 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -426,10 +426,6 @@ Mesh extractGroupsByName(const Mesh& mesh, const std::vector& group std::map coordRemap; std::map> groupCoordIds; - for (const auto& groupName : groupNames) { - groupCoordIds[groupName].clear(); - } - for (const auto& groupName : groupNames) { auto it = std::find_if(mesh.groups.begin(), mesh.groups.end(), [&groupName](const Group& g) { return g.name == groupName; }); diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 536e8f0..5b3f3e4 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -17,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) @@ -30,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}, @@ -51,15 +51,6 @@ TEST_F(LauncherTest, parse_rectilinear_grid) } } -TEST_F(LauncherTest, launches_alhambra_case) -{ - 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); -} - TEST_F(LauncherTest, builds_staircased_mesher_default) { meshlib::Mesh meshMock; @@ -68,9 +59,15 @@ TEST_F(LauncherTest, builds_staircased_mesher_default) 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 = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline_legacy.tessellator.json", objDef); + auto mesher = buildMesher(meshMock, j, objDef); EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); @@ -86,9 +83,15 @@ TEST_F(LauncherTest, builds_staircased_mesher_without_compression) 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 = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline.tessellator.json", objDef); + auto mesher = buildMesher(meshMock, j, objDef); EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); @@ -104,22 +107,37 @@ TEST_F(LauncherTest, builds_staircased_mesher_with_compression) 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 = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline_compression.tessellator.json", 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, true); } +TEST_F(LauncherTest, launches_alhambra_case) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/alhambra/alhambra.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + 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); } @@ -129,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); } @@ -138,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); } @@ -147,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); } @@ -156,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); } @@ -165,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); } @@ -174,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); } @@ -183,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); } @@ -192,13 +210,19 @@ 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) { - auto objects = readObjectsFromJSON("testData/cases/multiObject/basic.tessellator.json"); + 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"); @@ -212,7 +236,13 @@ TEST_F(LauncherTest, readObjectsFromJSON_basic) TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) { - auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); + 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"); @@ -235,7 +265,13 @@ TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) TEST_F(LauncherTest, readObjectsFromJSON_singleObject) { - auto objects = readObjectsFromJSON("testData/cases/multiObject/singleObject.tessellator.json"); + 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"); @@ -244,7 +280,13 @@ TEST_F(LauncherTest, readObjectsFromJSON_singleObject) TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) { - auto objects = readObjectsFromJSON("testData/cases/sphere/closed_sphere.tessellator.json"); + 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"); @@ -260,11 +302,16 @@ TEST_F(LauncherTest, builds_staircased_mesher_with_override) std::vector{0, 1} }; - std::string filename = "testData/cases/multiObject/mixedMesher.tessellator.json"; + std::string fileName = "testData/cases/multiObject/mixedMesher.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } - auto objects = readObjectsFromJSON(filename); + auto objects = readObjectsFromJSON(j); - auto mesher = meshlib::app::buildMesher(meshMock, filename, objects[0]); + auto mesher = meshlib::app::buildMesher(meshMock, j, objects[0]); EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); @@ -277,7 +324,7 @@ 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 = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -286,7 +333,7 @@ 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 = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -295,7 +342,7 @@ 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 = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -304,7 +351,7 @@ 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 = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); }