diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 73d442b..0123120 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -5,6 +5,9 @@ on: branches: - main - dev + +permissions: + contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -12,6 +15,8 @@ concurrency: jobs: builds-and-tests: + env: + VCPKG_BINARY_SOURCES: clear strategy: matrix: preset: [ @@ -69,4 +74,4 @@ jobs: run: build/bin/tessellator_tests - \ No newline at end of file + diff --git a/.gitignore b/.gitignore index 0a08b75..3f18a66 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ CMakeUserPresets.json sliced.vtk contour.vtk -testData/ \ No newline at end of file +testData/ +build diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..542abba --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,65 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "tessellator (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/tessellator", + "args": ["-i", "testData/cases/alhambra/alhambra.tessellator.json"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "visualizerFile": [ + "${workspaceFolder}/resources/Eigen.natvis", + "${workspaceFolder}/resources/nlohmann_json.natvis" + ], + "additionalSOLibSearchPath": "", + "showDisplayString": true, + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + }, + { + "name": "tessellator_tests (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/tesselator_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "visualizerFile": [ + "${workspaceFolder}/resources/Eigen.natvis", + "${workspaceFolder}/resources/nlohmann_json.natvis" + ], + "additionalSOLibSearchPath": "", + "showDisplayString": true, + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json index 322d7c9..5be2e29 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -26,7 +26,20 @@ "name": "gnu", "displayName": "GNU g++ compiler", "generator": "Ninja", - "inherits": "default" + "inherits": "default", + "cacheVariables": { + "TESSELLATOR_ENABLE_TESTS": "ON", + "TESSELLATOR_ENABLE_CGAL": "OFF" + } + }, + { + "name": "gnu-dbg", + "displayName": "GNU g++ compiler - Debug", + "inherits": "gnu", + "binaryDir": "build-dbg/", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } }, { "name": "docker", 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/resources/Eigen.natvis b/resources/Eigen.natvis new file mode 100644 index 0000000..dbed89e --- /dev/null +++ b/resources/Eigen.natvis @@ -0,0 +1,253 @@ + + + + + + + + [{$T2}, {$T3}] + + + 2 + $i==0 ? $T2 : $T3 + m_storage.m_data.array + + + Backward + 2 + $i==0 ? $T2 : $T3 + m_storage.m_data.array + + + + + + + [2, 2] + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[2]} + + + {m_storage.m_data.array[2]} {m_storage.m_data.array[3]} + + + {m_storage.m_data.array[1]} {m_storage.m_data.array[3]} + + + + + + + [3, 3] + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[3]} {m_storage.m_data.array[6]} + + + {m_storage.m_data.array[3]} {m_storage.m_data.array[4]} {m_storage.m_data.array[5]} + + + {m_storage.m_data.array[1]} {m_storage.m_data.array[4]} {m_storage.m_data.array[7]} + + + {m_storage.m_data.array[6]} {m_storage.m_data.array[7]} {m_storage.m_data.array[8]} + + + {m_storage.m_data.array[2]} {m_storage.m_data.array[5]} {m_storage.m_data.array[8]} + + + + + + + [3, 4] + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} {m_storage.m_data.array[3]} + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[3]} {m_storage.m_data.array[6]} {m_storage.m_data.array[9]} + + + {m_storage.m_data.array[4]} {m_storage.m_data.array[5]} {m_storage.m_data.array[6]} {m_storage.m_data.array[7]} + + + {m_storage.m_data.array[1]} {m_storage.m_data.array[4]} {m_storage.m_data.array[7]} {m_storage.m_data.array[10]} + + + {m_storage.m_data.array[8]} {m_storage.m_data.array[9]} {m_storage.m_data.array[10]} {m_storage.m_data.array[11]} + + + {m_storage.m_data.array[2]} {m_storage.m_data.array[5]} {m_storage.m_data.array[8]} {m_storage.m_data.array[11]} + + + + + + + [4, 4] + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} {m_storage.m_data.array[3]} + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[4]} {m_storage.m_data.array[8]} {m_storage.m_data.array[12]} + + + {m_storage.m_data.array[4]} {m_storage.m_data.array[5]} {m_storage.m_data.array[6]} {m_storage.m_data.array[7]} + + + {m_storage.m_data.array[1]} {m_storage.m_data.array[5]} {m_storage.m_data.array[9]} {m_storage.m_data.array[13]} + + + {m_storage.m_data.array[8]} {m_storage.m_data.array[9]} {m_storage.m_data.array[10]} {m_storage.m_data.array[11]} + + + {m_storage.m_data.array[2]} {m_storage.m_data.array[6]} {m_storage.m_data.array[10]} {m_storage.m_data.array[14]} + + + {m_storage.m_data.array[12]} {m_storage.m_data.array[13]} {m_storage.m_data.array[14]} {m_storage.m_data.array[15]} + + + {m_storage.m_data.array[3]} {m_storage.m_data.array[7]} {m_storage.m_data.array[11]} {m_storage.m_data.array[15]} + + + + + + + + empty + [{m_storage.m_rows}, {m_storage.m_cols}] (dynamic matrix) + + + 2 + $i==0 ? m_storage.m_rows : m_storage.m_cols + m_storage.m_data + + + Backward + 2 + $i==0 ? m_storage.m_rows : m_storage.m_cols + m_storage.m_data + + + + + + + empty + [{$T2}, {m_storage.m_cols}] (dynamic column matrix) + + + 2 + $i==0 ? $T2 : m_storage.m_cols + m_storage.m_data + + + Backward + 2 + $i==0 ? $T2 : m_storage.m_cols + m_storage.m_data + + + + + + + + empty + [{m_storage.m_rows}, {$T2}] (dynamic row matrix) + + + 2 + $i==0 ? m_storage.m_rows : $T2 + m_storage.m_data + + + Backward + 2 + $i==0 ? m_storage.m_rows : $T2 + m_storage.m_data + + + + + + + + empty + [{m_storage.m_cols}] (dynamic column vector) + + m_storage.m_cols + + m_storage.m_cols + m_storage.m_data + + + + + + + + empty + [{m_storage.m_rows}] (dynamic row vector) + + m_storage.m_rows + + m_storage.m_rows + m_storage.m_data + + + + + + + + [1] {m_storage.m_data.array[0]} + + m_storage.m_data.array[0] + + + + + + + [2] {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} + + m_storage.m_data.array[0] + m_storage.m_data.array[1] + + + + + + + [3] {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} + + m_storage.m_data.array[0] + m_storage.m_data.array[1] + m_storage.m_data.array[2] + + + + + + + [4] {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} {m_storage.m_data.array[3]} + + m_storage.m_data.array[0] + m_storage.m_data.array[1] + m_storage.m_data.array[2] + m_storage.m_data.array[3] + + + + diff --git a/resources/nlohmann_json.natvis b/resources/nlohmann_json.natvis new file mode 100644 index 0000000..5449cae --- /dev/null +++ b/resources/nlohmann_json.natvis @@ -0,0 +1,278 @@ + + + + + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 6012a8c..a6ee7f9 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,10 +1,5 @@ message(STATUS "Creating build system for tessellator-app") -add_library(tessellator-app - "vtkIO.cpp" - "launcher.cpp" -) - find_package(VTK COMPONENTS CommonCore IOGeometry @@ -12,18 +7,28 @@ find_package(VTK COMPONENTS FiltersCore ) -find_package(Boost COMPONENTS program_options) +if(VTK_FOUND) + add_library(tessellator-app + "vtkIO.cpp" + "launcher.cpp" + ) -find_package(nlohmann_json) + find_package(Boost COMPONENTS program_options) -target_link_libraries(tessellator-app - ${VTK_LIBRARIES} - Boost::program_options - nlohmann_json::nlohmann_json -) + find_package(nlohmann_json) -add_executable(tessellator - "tessellator.cpp" -) + target_link_libraries(tessellator-app + ${VTK_LIBRARIES} + Boost::program_options + nlohmann_json::nlohmann_json + ) + + add_executable(tessellator + "tessellator.cpp" + ) -target_link_libraries(tessellator tessellator-app tessellator-meshers) + target_link_libraries(tessellator tessellator-app tessellator-meshers) +else() + message(STATUS "VTK not found - tessellator app will not be built") + add_library(tessellator-app INTERFACE) +endif() diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index b8dd444..caf6108 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"].get(); + objDef.group = obj.value("group", std::filesystem::path(objDef.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"].get(); + objDef.group = std::filesystem::path(objDef.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) { @@ -95,27 +152,81 @@ std::string readExtension(const std::string &fn) } } -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["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; } -std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) + +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; + { + 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("exportGrid")) { + return mesherConfig["options"]["exportGrid"]; + } + return true; +} + +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}); + bool compress = readStaircaseMesherCompressOption(fn, override); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, compress}); } 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"); } @@ -138,25 +249,41 @@ 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); - 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; } -} \ No newline at end of file +} 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 9457e14..d55a561 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -1,6 +1,5 @@ #include "vtkIO.h" -#include #include #include #include @@ -8,6 +7,7 @@ #include #include #include +#include #include #include @@ -43,7 +43,7 @@ vtkSmartPointer readAsVTU(const std::filesystem::path& file } vtkSmartPointer vtu; - std::string extension = vtksys::SystemTools::GetFilenameLastExtension(fn); + std::string extension = fn.substr(fn.find_last_of(".")).empty() ? "" : fn.substr(fn.find_last_of(".")); std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); @@ -163,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/core/CMakeLists.txt b/src/core/CMakeLists.txt index d481f43..142beee 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -2,10 +2,12 @@ message(STATUS "Creating build system for tessellator-core") add_library(tessellator-core "Collapser.cpp" + "Compressor.cpp" "Slicer.cpp" "Snapper.cpp" "Smoother.cpp" "SmootherTools.cpp" + "Splitter.cpp" "Staircaser.cpp" ) diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp new file mode 100644 index 0000000..71c0f33 --- /dev/null +++ b/src/core/Compressor.cpp @@ -0,0 +1,757 @@ +#include "Compressor.h" + +#include +#include + +#include "utils/Geometry.h" +#include "utils/GridTools.h" + +namespace meshlib::core { + +using meshlib::Sign; +using meshlib::PlanePoint; +using meshlib::PlaneLinel; +using meshlib::PlaneSurfel; +using meshlib::PlaneSurface; +using meshlib::Contour; +using meshlib::CrossLine; + +std::size_t Compressor::compressSurfaces(Mesh& mesh) { + std::size_t totalOriginal = 0; + std::size_t totalCompressed = 0; + + for (GroupId g = 0; g < mesh.groups.size(); g++) { + std::vector surfs; + std::vector surfIndices; + + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + const Element& elem = mesh.groups[g].elements[e]; + if (elem.type == Element::Type::Surface) { + surfIndices.push_back(e); + surfs.push_back(elem); + } + } + + if (surfs.empty()) { + continue; + } + + totalOriginal += surfs.size(); + std::vector compressedSurfs = compressSurfs_(mesh.coordinates, surfs); + totalCompressed += compressedSurfs.size(); + + // Build new elements vector with compressed surfaces + std::vector newElements; + ElementId surfIdx = 0; + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + if (mesh.groups[g].elements[e].type == Element::Type::Surface) { + if (surfIdx < compressedSurfs.size()) { + newElements.push_back(compressedSurfs[surfIdx]); + surfIdx++; + } + } else { + newElements.push_back(mesh.groups[g].elements[e]); + } + } + mesh.groups[g].elements = std::move(newElements); + } + + return totalOriginal - totalCompressed; +} + +std::size_t Compressor::compressLines(Mesh& mesh) { + std::size_t totalOriginal = 0; + std::size_t totalCompressed = 0; + + for (GroupId g = 0; g < mesh.groups.size(); g++) { + std::vector lines; + + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + const Element& elem = mesh.groups[g].elements[e]; + if (elem.type == Element::Type::Line) { + lines.push_back(elem); + } + } + + if (lines.empty()) { + continue; + } + + totalOriginal += lines.size(); + std::vector compressedLines = compressLines_(mesh.coordinates, lines); + totalCompressed += compressedLines.size(); + + // Build new elements vector with compressed lines + std::vector newElements; + ElementId lineIdx = 0; + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + if (mesh.groups[g].elements[e].type == Element::Type::Line) { + if (lineIdx < compressedLines.size()) { + newElements.push_back(compressedLines[lineIdx]); + lineIdx++; + } + } else { + newElements.push_back(mesh.groups[g].elements[e]); + } + } + mesh.groups[g].elements = std::move(newElements); + } + + return totalOriginal - totalCompressed; +} + +std::vector Compressor::compressLines_( + const std::vector& coords, + const std::vector& lines) { + std::vector res; + std::map, + std::pair>, + std::vector> signDirLines; + for (std::size_t l = 0; l < lines.size(); l++) { + std::array auxCells; + auxCells[0] = utils::GridTools::toCell(coords[lines[l].vertices[0]]); + auxCells[1] = utils::GridTools::toCell(coords[lines[l].vertices[1]]); + std::array gridLine; + Sign sign = 1; + Axis dir = 0; + for (Axis d = 0; d < 3; d++) { + if (auxCells[0](d) != auxCells[1](d)) { + if (auxCells[0](d) > auxCells[1](d)) { + sign = -1; + } + dir = d; + Axis d1 = (d + 1) % 3; + Axis d2 = (d + 2) % 3; + gridLine[0] = auxCells[0](d1); + gridLine[1] = auxCells[0](d2); + break; + } + } + signDirLines[std::make_pair(gridLine, + std::make_pair(sign, dir))].push_back(l); + } + for (std::map, + std::pair>, + std::vector>::const_iterator + it = signDirLines.begin(); it != signDirLines.end(); ++it) { + std::vector auxElems; + for (std::size_t i = 0; i < it->second.size(); i++) { + auxElems.push_back(lines[it->second[i]]); + } + std::vector auxRes = + compressDirSignLines_(coords, + it->first.second, + auxElems); + res.insert(res.end(), auxRes.begin(), auxRes.end()); + } + return res; +} + +std::vector Compressor::compressDirSignLines_( + const std::vector& coords, + const std::pair& signDir, + const std::vector& lines) { + std::vector res; + std::map> coordLines; + std::map> lineCoords; + for (std::size_t l = 0; l < lines.size(); l++) { + for (std::size_t v = 0; v < 2; v++) { + coordLines[lines[l].vertices[v]].insert(l); + lineCoords[l].insert(lines[l].vertices[v]); + } + } + std::set vis; + for (std::map>::const_iterator + itExt = lineCoords.begin(); itExt != lineCoords.end(); ++itExt) { + if (vis.count(itExt->first) == 0) { + CoordinateId minCell = lines[itExt->first].vertices[0]; + CoordinateId maxCell = lines[itExt->first].vertices[1]; + std::queue q; + q.push(itExt->first); + vis.insert(itExt->first); + while (!q.empty()) { + ElementId elem = q.front(); + q.pop(); + for (std::size_t i = 0; i < 2; i++) { + if (coords[minCell] > coords[lines[elem].vertices[i]]) { + minCell = lines[elem].vertices[i]; + } + if (coords[maxCell] < coords[lines[elem].vertices[i]]) { + maxCell = lines[elem].vertices[i]; + } + } + for (std::set::const_iterator + itCell = lineCoords[elem].begin(); + itCell != lineCoords[elem].end(); ++itCell) { + for (std::set::const_iterator + itLine = coordLines[*itCell].begin(); + itLine != coordLines[*itCell].end(); ++itLine) { + if (vis.count(*itLine) == 0) { + q.push(*itLine); + vis.insert(*itLine); + } + } + } + } + Element newElem; + newElem.type = Element::Type::Line; + newElem.vertices.push_back(minCell); + newElem.vertices.push_back(maxCell); + if (signDir.first < 0) { + std::swap(newElem.vertices[0], newElem.vertices[1]); + } + res.push_back(newElem); + } + } + return res; +} + +std::vector Compressor::compressSurfs_( + std::vector& coords, + const std::vector& surfs) { + std::vector res; + std::map>, + std::vector> signDirSurfs; + for (std::size_t s = 0; s < surfs.size(); s++) { + if (surfs[s].vertices.size() != 4) { + res.push_back(surfs[s]); + continue; + } + std::array auxCells; + auxCells[0] = utils::GridTools::toCell(coords[surfs[s].vertices[0]]); + auxCells[1] = utils::GridTools::toCell(coords[surfs[s].vertices[1]]); + auxCells[2] = utils::GridTools::toCell(coords[surfs[s].vertices[2]]); + CellDir gridSurf; + Sign sign = 1; + Axis dir = 0; + for (Axis d = 0; d < 3; d++) { + if (auxCells[0](d) == auxCells[2](d)) { + Cell normal = (auxCells[1] - auxCells[0]) ^ + (auxCells[2] - auxCells[0]); + if (normal(d) >= 0) { + sign = 1; + } else { + sign = -1; + } + dir = d; + gridSurf = auxCells[0](d); + break; + } + } + signDirSurfs[std::make_pair(gridSurf, + std::make_pair(sign, dir))].push_back(s); + } + for (std::map>, + std::vector>::const_iterator + it = signDirSurfs.begin(); it != signDirSurfs.end(); ++it) { + std::vector auxElems; + for (std::size_t i = 0; i < it->second.size(); i++) { + auxElems.push_back(surfs[it->second[i]]); + } + std::vector auxRes = + compressDirSignSurfs_(coords, + it->first.second, + auxElems); + res.insert(res.end(), auxRes.begin(), auxRes.end()); + } + return res; +} + +std::vector Compressor::compressDirSignSurfs_( + std::vector& coords, + const std::pair& signDir, + const std::vector& surfs) { + std::vector res; + std::map> lineSurfs; + std::map> surfLines; + for (std::size_t s = 0; s < surfs.size(); s++) { + for (std::size_t i = 0; i < 4; i++) { + std::size_t j = (i + 1) % 4; + LinIds line; + line[0] = surfs[s].vertices[i]; + line[1] = surfs[s].vertices[j]; + std::sort(line.begin(), line.end()); + lineSurfs[line].insert(s); + surfLines[s].insert(line); + } + } + std::set vis; + for (std::map>::const_iterator + itExt = surfLines.begin(); itExt != surfLines.end(); ++itExt) { + if (vis.count(itExt->first) == 0) { + std::set surfsConn; + std::queue q; + q.push(itExt->first); + vis.insert(itExt->first); + while (!q.empty()) { + ElementId elem = q.front(); + q.pop(); + surfsConn.insert(elem); + for (std::set::const_iterator + itLine = surfLines[elem].begin(); + itLine != surfLines[elem].end(); ++itLine) { + for (std::set::const_iterator + itSurf = lineSurfs[*itLine].begin(); + itSurf != lineSurfs[*itLine].end(); ++itSurf) { + if (vis.count(*itSurf) == 0) { + q.push(*itSurf); + vis.insert(*itSurf); + } + } + } + } + std::vector resCon; + for (std::set::const_iterator + it = surfsConn.begin(); it != surfsConn.end(); ++it) { + resCon.push_back(surfs[*it]); + } + resCon = compressSurf_(coords, signDir, resCon); + res.insert(res.end(), resCon.begin(), resCon.end()); + } + } + return res; +} + +std::vector Compressor::compressSurf_( + std::vector& coords, + const std::pair& signDir, + const std::vector& surfs) { + std::vector res; + Axis d = signDir.second; + Axis d1 = (d + 1) % 3; + Axis d2 = (d + 2) % 3; + CellDir plane = utils::GridTools::toCell(coords[surfs[0].vertices[0]])(d); + std::set surfels; + for (std::size_t s = 0; s < surfs.size(); s++) { + std::pair ext; + ext.first[0] = + utils::GridTools::toCell(coords[surfs[s].vertices[0]])(d1); + ext.first[1] = + utils::GridTools::toCell(coords[surfs[s].vertices[0]])(d2); + ext.second[0] = + utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d1); + ext.second[1] = + utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d2); + CellDir i0 = std::min(ext.first[0], ext.second[0]); + CellDir i1 = std::max(ext.first[0], ext.second[0]); + CellDir j0 = std::min(ext.first[1], ext.second[1]); + CellDir j1 = std::max(ext.first[1], ext.second[1]); + for (CellDir i = i0; i < i1; i++) { + for (CellDir j = j0; j < j1; j++) { + PlaneSurfel surfel = {{i, j}}; + surfels.insert(surfel); + } + } + } + std::vector aux = compressSurfels_(surfels); + CoordinateMap coordMap; + for (std::size_t s = 0; s < surfs.size(); s++) { + for (std::size_t i = 0; i < 4; i++) { + CoordinateId coordId = surfs[s].vertices[i]; + Coordinate coord = coords[coordId]; + coordMap[coord] = coordId; + } + } + for (std::size_t e = 0; e < aux.size(); e++) { + std::array ext; + ext[0](d) = ext[2](d) = plane; + ext[0](d1) = aux[e].first[0]; + ext[0](d2) = aux[e].first[1]; + ext[2](d1) = aux[e].second[0]; + ext[2](d2) = aux[e].second[1]; + ext[1] = ext[3] = ext[0]; + ext[1](d1) = ext[2](d1); + ext[3](d2) = ext[2](d2); + if (signDir.first < 0) { + std::swap(ext[1], ext[3]); + } + Element resElem; + resElem.type = Element::Type::Surface; + for (std::size_t i = 0; i < 4; i++) { + Relative rel = utils::GridTools::toRelative(ext[i]); + if (coordMap.count(rel) == 0) { + coordMap[rel] = coords.size(); + coords.push_back(rel); + } + resElem.vertices.push_back(coordMap[rel]); + } + res.push_back(resElem); + } + return res; +} + +std::vector Compressor::compressSurfels_( + const std::set& surfs) { + std::vector res; + const std::vector& conts = getContours_(surfs); + std::array, 2> cross = + getCrossingLines_(surfs, conts); + cross = getMaxCompatLines_(cross); + std::set linels; + for (std::size_t c = 0; c < conts.size(); c++) { + for (std::size_t i = 0; i < conts[c].size(); i++) { + std::size_t j = (i + 1) % conts[c].size(); + std::set aux = getLinels_(conts[c][i], conts[c][j]); + linels.insert(aux.begin(), aux.end()); + } + } + for (Axis d = 0; d < 2; d++) { + Axis d1 = (d + 1) % 2; + for (std::size_t i = 0; i < cross[d].size(); i++) { + PlanePoint ini, end; + ini[d1] = end[d1] = cross[d][i].first; + ini[d] = cross[d][i].second.first; + end[d] = cross[d][i].second.second; + std::set aux = getLinels_(ini, end); + linels.insert(aux.begin(), aux.end()); + } + } + addConcaveLinels_(surfs, conts, linels); + std::map> lineSurfs; + std::map> surfLines; + for (std::set::const_iterator + it = surfs.begin(); it != surfs.end(); ++it) { + surfLines.insert(std::make_pair(*it, std::set())); + for (Axis d = 0; d < 2; d++) { + for (CellDir diff = -1; diff <= 1; diff += 2) { + PlaneLinel linel = getSurfaceEdge_(*it, diff, d); + if (linels.count(linel) == 0) { + surfLines[*it].insert(linel); + lineSurfs[linel].insert(*it); + } + } + } + } + std::set vis; + for (std::map>::const_iterator + itSurfExt = surfLines.begin(); + itSurfExt != surfLines.end(); ++itSurfExt) { + if (vis.count(itSurfExt->first) == 0) { + std::queue q; + q.push(itSurfExt->first); + vis.insert(itSurfExt->first); + PlanePoint minPoint = itSurfExt->first; + PlanePoint maxPoint = itSurfExt->first; + while (!q.empty()) { + PlaneSurfel surfel = q.front(); + q.pop(); + if (surfel < minPoint) { + minPoint = surfel; + } + if (surfel > maxPoint) { + maxPoint = surfel; + } + for (std::set::const_iterator + itLin = surfLines[surfel].begin(); + itLin != surfLines[surfel].end(); ++itLin) { + for (std::set::const_iterator + itSurfInt = lineSurfs[*itLin].begin(); + itSurfInt != lineSurfs[*itLin].end(); ++itSurfInt) { + if (vis.count(*itSurfInt) == 0) { + q.push(*itSurfInt); + vis.insert(*itSurfInt); + } + } + } + } + maxPoint[0]++; + maxPoint[1]++; + res.push_back(std::make_pair(minPoint, maxPoint)); + } + } + return res; +} + +std::vector Compressor::getContours_( + const std::set& surfs) { + std::vector res; + if (surfs.empty()) { + return res; + } + std::set vis; + res.push_back( + getContour_(getSurfaceEdge_(*surfs.begin(), -1, 0), surfs, vis)); + for (std::set::const_iterator + it = surfs.begin(); it != surfs.end(); ++it) { + for (Axis d = 0; d < 2; d++) { + for (CellDir diff = -1; diff <= 1; diff += 2) { + PlaneSurfel adjSurf; + PlaneLinel adjEdge; + adjSurf = *it; + adjSurf[d] += diff; + adjEdge = getSurfaceEdge_(*it, diff, d); + if ((surfs.find(adjSurf) == surfs.end()) && + (vis.find(adjEdge) == vis.end())) { + res.push_back(getContour_(adjEdge, surfs, vis)); + } + } + } + } + return res; +} + +Contour Compressor::getContour_( + const PlaneLinel& from, + const std::set& surfs, + std::set& vis) { + Contour res; + std::queue q; + if (vis.find(from) != vis.end()) { + return res; + } + std::vector lines; + q.push(from); + vis.insert(from); + lines.push_back(from); + while (!q.empty()) { + PlaneLinel edge = q.front(); + q.pop(); + PlanePoint pos = edge.first; + Axis d0 = edge.second; + Axis d1 = (d0 + 1) % 2; + PlaneSurfel surf = pos; + if (surfs.find(surf) == surfs.end()) { + surf[d1]--; + } + for (CellDir diff = -1; diff <= 1; diff += 2) { + PlaneSurfel adjSurf1 = surf; + adjSurf1[d0] += diff; + if (surfs.find(adjSurf1) == surfs.end()) { + PlaneLinel adjEdge = getSurfaceEdge_(surf, diff, d0); + if (vis.find(adjEdge) == vis.end()) { + q.push(adjEdge); + vis.insert(adjEdge); + lines.push_back(adjEdge); + break; + } + continue; + } + if (surf == pos) { + adjSurf1[d1]--; + } else { + adjSurf1[d1]++; + } + if (surfs.find(adjSurf1) == surfs.end()) { + PlaneLinel adjEdge = edge; + adjEdge.first[d0] += diff; + if (vis.find(adjEdge) == vis.end()) { + q.push(adjEdge); + vis.insert(adjEdge); + lines.push_back(adjEdge); + break; + } + continue; + } else { + PlaneLinel adjEdge = getSurfaceEdge_(adjSurf1, -diff, d0); + if (vis.find(adjEdge) == vis.end()) { + q.push(adjEdge); + vis.insert(adjEdge); + lines.push_back(adjEdge); + break; + } + continue; + } + } + } + for (std::vector::const_iterator + it = lines.begin(); it != lines.end(); ++it) { + std::vector::const_iterator itPlus = std::next(it); + if (itPlus == lines.end()) { + itPlus = lines.begin(); + } + if (it->second == itPlus->second) { + continue; + } + std::array extremes = {it->first, it->first}; + extremes[1][it->second]++; + std::array extremesP = {itPlus->first, itPlus->first}; + extremesP[1][itPlus->second]++; + for (std::size_t p = 0; p < 4; p++) { + if (extremes[p / 2] == extremesP[p % 2]) { + res.push_back(extremes[p / 2]); + break; + } + } + } + return res; +} + +PlaneLinel Compressor::getSurfaceEdge_(const PlaneSurfel& surf, + const CellDir& diff, + const Axis& dir) { + PlaneLinel res; + res.first = surf; + res.second = (dir + 1) % 2; + if (diff > 0) { + res.first[dir]++; + } + return res; +} + +std::array, 2> + Compressor::getCrossingLines_( + const std::set& surfs, + const std::vector& conts) { + std::array, 2> res; + for (Axis d = 0; d < 2; d++) { + Axis d1 = (d + 1) % 2; + std::map> cross; + for (std::vector::const_iterator + it1 = conts.begin(); it1 != conts.end(); ++it1) { + for (std::vector::const_iterator + it2 = it1->begin(); it2 != it1->end(); ++it2) { + cross[(*it2)[d1]].insert((*it2)[d]); + } + } + for (std::map>::const_iterator + itMap = cross.begin(); itMap != cross.end(); ++itMap) { + for (std::set::const_iterator + itSet = itMap->second.begin(); + itSet != itMap->second.end(); ++itSet) { + std::set::const_iterator itSetPlus = std::next(itSet); + if (itSetPlus == itMap->second.end()) { + break; + } + bool valid = true; + PlanePoint edge; + edge[d1] = itMap->first; + for (CellDir i = *itSet; i < *itSetPlus; i++) { + edge[d] = i; + PlaneSurfel adjSurf1, adjSurf2; + adjSurf1 = adjSurf2 = edge; + adjSurf1[d1]--; + if ((surfs.find(adjSurf1) == surfs.end()) || + (surfs.find(adjSurf2) == surfs.end())) { + valid = false; + break; + } + } + if (valid) { + res[d].push_back( + std::make_pair(itMap->first, + std::make_pair(*itSet, *itSetPlus))); + } + } + } + } + return res; +} + +std::array, 2> + Compressor::getMaxCompatLines_( + const std::array, 2>& cross) { + std::array, 2> res; + if (cross[0].size() > cross[1].size()) { + res[0] = cross[0]; + } else { + res[1] = cross[1]; + } + return res; +} + +std::set Compressor::getLinels_( + const PlanePoint& ini, + const PlanePoint& end) { + std::set res; + for (Axis d = 0; d < 2; d++) { + Axis d1 = (d + 1) % 2; + if (ini[d] == end[d]) { + PlaneLinel linel; + linel.first[d] = ini[d]; + linel.second = d1; + for (CellDir + k = std::min(ini[d1], end[d1]); + k < std::max(ini[d1], end[d1]); k++) { + linel.first[d1] = k; + res.insert(linel); + } + } + } + return res; +} + +void Compressor::addConcaveLinels_(const std::set& surfs, + const std::vector& conts, + std::set& lines) { + std::set concavePoints; + for (std::size_t c = 0; c < conts.size(); c++) { + for (std::size_t i = 0; i < conts[c].size(); i++) { + PlanePoint point = conts[c][i]; + std::size_t numSurfAdj = 0; + std::size_t numLineAdj = 0; + for (CellDir diffx = -1; diffx < 1; diffx++) { + for (CellDir diffy = -1; diffy < 1; diffy++) { + PlaneSurfel surfel = point; + surfel[0] += diffx; + surfel[1] += diffy; + if (surfs.count(surfel) != 0) { + numSurfAdj++; + } + } + } + for (Axis d = 0; d < 2; d++) { + for (CellDir diff = -1; diff < 1; diff++) { + PlaneLinel linel = std::make_pair(point, d); + linel.first[d] += diff; + if (lines.count(linel) != 0) { + numLineAdj++; + } + } + } + if ((numSurfAdj > 2) && (numLineAdj < 3)) { + concavePoints.insert(point); + } + } + } + for (std::set::const_iterator + it = concavePoints.begin(); it != concavePoints.end(); ++it) { + std::size_t numLineAdj = 0; + for (Axis d = 0; d < 2; d++) { + for (CellDir diff = -1; diff < 1; diff++) { + PlaneLinel linel = std::make_pair(*it, d); + linel.first[d] += diff; + if (lines.count(linel) != 0) { + numLineAdj++; + } + } + } + if (numLineAdj > 2) { + continue; + } + for (Axis d = 0; d < 2; d++) { + Axis d1 = (d + 1) % 2; + bool found = false; + for (CellDir diff = -1; diff < 1; diff++) { + PlaneLinel linel = std::make_pair(*it, d); + linel.first[d] += diff; + if (lines.count(linel) == 0) { + lines.insert(linel); + while (true) { + PlaneLinel aux1, aux2; + if (diff < 0) { + aux1 = aux2 = std::make_pair(linel.first, d1); + aux1.first[d1]--; + linel.first[d]--; + } else { + linel.first[d]++; + aux1 = aux2 = std::make_pair(linel.first, d1); + aux1.first[d1]--; + } + if ((lines.count(aux1) != 0) || + (lines.count(aux2) != 0)) { + break; + } + lines.insert(linel); + } + found = true; + break; + } + } + if (found) { + break; + } + } + } +} + +} diff --git a/src/core/Compressor.h b/src/core/Compressor.h new file mode 100644 index 0000000..5b66e32 --- /dev/null +++ b/src/core/Compressor.h @@ -0,0 +1,90 @@ +#pragma once + +#include "types/Mesh.h" +#include "utils/Types.h" + +#include +#include + +namespace meshlib::core { + +class Compressor { +public: + // Compress only quad surfaces (4 vertices) that are coplanar and adjacent + // Returns number of surfaces merged (original_count - compressed_count) + static std::size_t compressSurfaces(Mesh& mesh); + + // Compress collinear line segments that are adjacent + // Returns number of lines merged (original_count - compressed_count) + static std::size_t compressLines(Mesh& mesh); + +private: + // Group surfaces by (grid_plane, sign, axis) and compress each group + static std::vector compressSurfs_( + std::vector& coords, + const std::vector& surfs); + + // Compress surfaces with same normal direction and sign + static std::vector compressDirSignSurfs_( + std::vector& coords, + const std::pair& signDir, + const std::vector& surfs); + + // Compress connected coplanar surfaces using contour detection + static std::vector compressSurf_( + std::vector& coords, + const std::pair& signDir, + const std::vector& surfs); + + // Group lines by (grid_line, sign, axis) and compress each group + static std::vector compressLines_( + const std::vector& coords, + const std::vector& lines); + + // Compress lines with same direction and sign + static std::vector compressDirSignLines_( + const std::vector& coords, + const std::pair& signDir, + const std::vector& lines); + + // Merge adjacent surfels into maximal rectangles + static std::vector compressSurfels_( + const std::set& surfs); + + // Detect boundary contours of surfel set + static std::vector getContours_(const std::set& surfs); + + // Trace a single contour from starting edge + static Contour getContour_( + const PlaneLinel& from, + const std::set& surfs, + std::set& visited); + + // Get edge of a surfel in given direction + static PlaneLinel getSurfaceEdge_( + const PlaneSurfel& surf, + const CellDir& diff, + const Axis& dir); + + // Find lines crossing between contours + static std::array, 2> getCrossingLines_( + const std::set& surfs, + const std::vector& contours); + + // Select the set of crossing lines with maximum count + static std::array, 2> getMaxCompatLines_( + const std::array, 2>& cross); + + // Get linels between two points + static std::set getLinels_( + const PlanePoint& ini, + const PlanePoint& end); + + // Add linels at concave corners + static void addConcaveLinels_( + const std::set& surfs, + const std::vector& contours, + std::set& lines); +}; + +} diff --git a/src/core/Splitter.cpp b/src/core/Splitter.cpp new file mode 100644 index 0000000..059b5a7 --- /dev/null +++ b/src/core/Splitter.cpp @@ -0,0 +1,329 @@ +#include "Splitter.h" + +#include "utils/GridTools.h" + +namespace meshlib::core { + +std::size_t Splitter::splitSurfaces(Mesh& mesh) { + std::size_t totalNewQuads = 0; + + for (GroupId g = 0; g < mesh.groups.size(); g++) { + std::vector newElements; + + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + const Element& elem = mesh.groups[g].elements[e]; + if (elem.type == Element::Type::Surface) { + // Split this surface into unit quads + std::map coordMap; + + // Build initial coord map from existing coordinates + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + coordMap[mesh.coordinates[i]] = i; + } + + std::vector splitQuads = splitSurface_( + elem, mesh.coordinates, mesh.grid, coordMap); + + // Add new coordinates from coordMap + for (const auto& [coord, id] : coordMap) { + if (id >= mesh.coordinates.size()) { + mesh.coordinates.push_back(coord); + } + } + + newElements.insert(newElements.end(), splitQuads.begin(), splitQuads.end()); + totalNewQuads += splitQuads.size(); + } else { + newElements.push_back(elem); + } + } + + mesh.groups[g].elements = std::move(newElements); + } + + return totalNewQuads; +} + +std::size_t Splitter::splitLines(Mesh& mesh) { + std::size_t totalNewLines = 0; + + for (GroupId g = 0; g < mesh.groups.size(); g++) { + std::vector newElements; + + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + const Element& elem = mesh.groups[g].elements[e]; + if (elem.type == Element::Type::Line) { + // Split this line into unit grid lines + std::map coordMap; + + // Build initial coord map from existing coordinates + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + coordMap[mesh.coordinates[i]] = i; + } + + std::vector splitLines = splitLine_( + elem, mesh.coordinates, mesh.grid, coordMap); + + // Add new coordinates from coordMap + for (const auto& [coord, id] : coordMap) { + if (id >= mesh.coordinates.size()) { + mesh.coordinates.push_back(coord); + } + } + + newElements.insert(newElements.end(), splitLines.begin(), splitLines.end()); + totalNewLines += splitLines.size(); + } else { + newElements.push_back(elem); + } + } + + mesh.groups[g].elements = std::move(newElements); + } + + return totalNewLines; +} + +std::vector Splitter::splitLine_( + const Element& line, + const std::vector& coords, + const Grid& grid, + std::map& coordMap) { + std::vector unitLines; + + // Get the axis direction of the line + Axis lineAxis = getLineAxis_(line, coords); + + // Get the grid cell bounds + auto [minCell, maxCell] = getLineBounds_(line, coords); + + // Determine the fixed coordinate axes (the two axes perpendicular to lineAxis) + Axis axis1 = (lineAxis + 1) % 3; + Axis axis2 = (lineAxis + 2) % 3; + + // Get the fixed coordinate values from minCell + CellDir fixedCoord1 = minCell(axis1); + CellDir fixedCoord2 = minCell(axis2); + + // Generate unit lines for each cell along the line axis + for (CellDir i = minCell(lineAxis); i < maxCell(lineAxis); i++) { + Element unitLine = createUnitLine_( + fixedCoord1, fixedCoord2, lineAxis, i, grid, coordMap); + unitLines.push_back(unitLine); + } + + return unitLines; +} + +std::pair Splitter::getLineBounds_( + const Element& line, + const std::vector& coords) { + Cell minCell = utils::GridTools::toCell(coords[line.vertices[0]]); + Cell maxCell = utils::GridTools::toCell(coords[line.vertices[1]]); + + // Ensure minCell <= maxCell for all axes + for (Axis d = 0; d < 3; d++) { + if (minCell(d) > maxCell(d)) { + std::swap(minCell(d), maxCell(d)); + } + } + + return {minCell, maxCell}; +} + +Axis Splitter::getLineAxis_( + const Element& line, + const std::vector& coords) { + // Find which axis has different coordinate values (the line direction) + Cell cell0 = utils::GridTools::toCell(coords[line.vertices[0]]); + Cell cell1 = utils::GridTools::toCell(coords[line.vertices[1]]); + + for (Axis d = 0; d < 3; d++) { + if (cell0(d) != cell1(d)) { + return d; + } + } + + // Fallback (should not happen for valid lines) + return 0; +} + +Element Splitter::createUnitLine_( + CellDir fixedCoord1, + CellDir fixedCoord2, + Axis lineAxis, + CellDir cell, + const Grid& grid, + std::map& coordMap) { + Axis axis1 = (lineAxis + 1) % 3; + Axis axis2 = (lineAxis + 2) % 3; + + // Create 2 coordinates for the unit line + std::array endpoints; + endpoints[0] = Coordinate({0, 0, 0}); + endpoints[1] = Coordinate({0, 0, 0}); + + // Set coordinates for each endpoint + endpoints[0](lineAxis) = grid[lineAxis][cell]; + endpoints[0](axis1) = grid[axis1][fixedCoord1]; + endpoints[0](axis2) = grid[axis2][fixedCoord2]; + + endpoints[1](lineAxis) = grid[lineAxis][cell + 1]; + endpoints[1](axis1) = grid[axis1][fixedCoord1]; + endpoints[1](axis2) = grid[axis2][fixedCoord2]; + + // Get or create coordinate IDs + std::array vids; + for (int i = 0; i < 2; i++) { + auto it = coordMap.find(endpoints[i]); + if (it != coordMap.end()) { + vids[i] = it->second; + } else { + coordMap[endpoints[i]] = static_cast(coordMap.size()); + vids[i] = coordMap[endpoints[i]]; + } + } + + Element unitLine; + unitLine.type = Element::Type::Line; + unitLine.vertices = {vids[0], vids[1]}; + + return unitLine; +} + +std::vector Splitter::splitSurface_( + const Element& surface, + const std::vector& coords, + const Grid& grid, + std::map& coordMap) { + std::vector quads; + + // Get the plane orientation of the surface + auto [normalAxis, plane] = getSurfacePlane_(surface, coords); + + // Get the grid cell bounds + auto [minCell, maxCell] = getSurfaceBounds_(surface, coords); + + // Determine the 2D axes in the plane + Axis axis1 = (normalAxis + 1) % 3; + Axis axis2 = (normalAxis + 2) % 3; + + // Generate unit quads for each cell in the bounds + for (CellDir i = minCell(axis1); i < maxCell(axis1); i++) { + for (CellDir j = minCell(axis2); j < maxCell(axis2); j++) { + Element quad = createUnitQuad_( + plane, normalAxis, i, j, grid, coordMap); + quads.push_back(quad); + } + } + + return quads; +} + +std::pair Splitter::getSurfaceBounds_( + const Element& surface, + const std::vector& coords) { + Cell minCell = {0, 0, 0}; + Cell maxCell = {0, 0, 0}; + + bool first = true; + for (CoordinateId vid : surface.vertices) { + Cell cell = utils::GridTools::toCell(coords[vid]); + if (first) { + minCell = cell; + maxCell = cell; + first = false; + } else { + for (Axis d = 0; d < 3; d++) { + minCell(d) = std::min(minCell(d), cell(d)); + maxCell(d) = std::max(maxCell(d), cell(d)); + } + } + } + + // maxCell represents the cell containing the max coordinate value + // For a surface spanning cells 0 to N-1, the max coordinate is at grid[N] + // So maxCell should be set to N (the cell index of the max coord), which is already correct + // The loop below will iterate i < maxCell, giving cells 0 to maxCell-1 + // No increment needed + + return {minCell, maxCell}; +} + +std::pair Splitter::getSurfacePlane_( + const Element& surface, + const std::vector& coords) { + // Find which axis has constant coordinate (the normal axis) + for (Axis d = 0; d < 3; d++) { + Cell cell0 = utils::GridTools::toCell(coords[surface.vertices[0]]); + bool allSame = true; + for (CoordinateId vid : surface.vertices) { + Cell cell = utils::GridTools::toCell(coords[vid]); + if (cell(d) != cell0(d)) { + allSame = false; + break; + } + } + if (allSame) { + return {d, cell0(d)}; + } + } + + // Fallback (should not happen for valid surfaces) + return {0, 0}; +} + +Element Splitter::createUnitQuad_( + CellDir plane, + Axis normalAxis, + CellDir xCell, + CellDir yCell, + const Grid& grid, + std::map& coordMap) { + Axis axis1 = (normalAxis + 1) % 3; + Axis axis2 = (normalAxis + 2) % 3; + + // Create 4 corner coordinates for the unit quad + std::array corners; + corners[0] = Coordinate({0, 0, 0}); + corners[1] = Coordinate({0, 0, 0}); + corners[2] = Coordinate({0, 0, 0}); + corners[3] = Coordinate({0, 0, 0}); + + // Set coordinates for each corner + corners[0](normalAxis) = grid[normalAxis][plane]; + corners[0](axis1) = grid[axis1][xCell]; + corners[0](axis2) = grid[axis2][yCell]; + + corners[1](normalAxis) = grid[normalAxis][plane]; + corners[1](axis1) = grid[axis1][xCell + 1]; + corners[1](axis2) = grid[axis2][yCell]; + + corners[2](normalAxis) = grid[normalAxis][plane]; + corners[2](axis1) = grid[axis1][xCell + 1]; + corners[2](axis2) = grid[axis2][yCell + 1]; + + corners[3](normalAxis) = grid[normalAxis][plane]; + corners[3](axis1) = grid[axis1][xCell]; + corners[3](axis2) = grid[axis2][yCell + 1]; + + // Get or create coordinate IDs + std::array vids; + for (int i = 0; i < 4; i++) { + auto it = coordMap.find(corners[i]); + if (it != coordMap.end()) { + vids[i] = it->second; + } else { + coordMap[corners[i]] = static_cast(coordMap.size()); + vids[i] = coordMap[corners[i]]; + } + } + + Element quad; + quad.type = Element::Type::Surface; + quad.vertices = {vids[0], vids[1], vids[2], vids[3]}; + + return quad; +} + +} diff --git a/src/core/Splitter.h b/src/core/Splitter.h new file mode 100644 index 0000000..91206ef --- /dev/null +++ b/src/core/Splitter.h @@ -0,0 +1,72 @@ +#pragma once + +#include "types/Mesh.h" +#include "utils/Types.h" + +namespace meshlib::core { + +class Splitter { +public: + // Split all surfaces in mesh into unit quads (1x1 grid cells) + // Returns number of new quads created + static std::size_t splitSurfaces(Mesh& mesh); + + // Split all lines in mesh into unit grid lines + // Returns number of new unit lines created + static std::size_t splitLines(Mesh& mesh); + +private: + // Split a single surface into unit quads + static std::vector splitSurface_( + const Element& surface, + const std::vector& coords, + const Grid& grid, + std::map& coordMap); + + // Get grid cell bounds for a surface + static std::pair getSurfaceBounds_( + const Element& surface, + const std::vector& coords); + + // Determine the plane orientation of a surface (which axis is normal) + static std::pair getSurfacePlane_( + const Element& surface, + const std::vector& coords); + + // Create a unit quad at given grid cell position + static Element createUnitQuad_( + CellDir plane, + Axis normalAxis, + CellDir xCell, + CellDir yCell, + const Grid& grid, + std::map& coordMap); + + // Split a single polyline into unit grid lines + static std::vector splitLine_( + const Element& line, + const std::vector& coords, + const Grid& grid, + std::map& coordMap); + + // Get grid cell bounds for a line + static std::pair getLineBounds_( + const Element& line, + const std::vector& coords); + + // Determine the axis direction of a line + static Axis getLineAxis_( + const Element& line, + const std::vector& coords); + + // Create a unit line at given grid cell position + static Element createUnitLine_( + CellDir fixedCoord1, + CellDir fixedCoord2, + Axis lineAxis, + CellDir cell, + const Grid& grid, + std::map& coordMap); +}; + +} diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 7ad6458..c0aac52 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -6,6 +6,7 @@ #include "core/Slicer.h" #include "core/Collapser.h" #include "core/Staircaser.h" +#include "core/Compressor.h" #include "utils/RedundancyCleaner.h" #include "utils/MeshTools.h" @@ -17,9 +18,10 @@ using namespace utils; using namespace core; using namespace meshTools; -StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser) : +StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, bool compress) : MesherBase(inputMesh), - decimalPlacesInCollapser_(decimalPlacesInCollapser) + decimalPlacesInCollapser_(decimalPlacesInCollapser), + compress_(compress) { log("Preparing surfaces."); surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -71,6 +73,24 @@ void StaircaseMesher::process(Mesh& mesh) const logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); logNumberOfLines(countMeshElementsIf(mesh, isLine)); + + if (compress_) { + log("Compressing surfaces.", 1); + std::size_t beforeQuads = countMeshElementsIf(mesh, isQuad); + std::size_t merged = Compressor::compressSurfaces(mesh); + std::size_t afterQuads = countMeshElementsIf(mesh, isQuad); + log("Compressed " + std::to_string(beforeQuads) + + " -> " + std::to_string(afterQuads) + + " quads (merged " + std::to_string(merged) + " surfaces)", 1); + + log("Compressing lines.", 1); + std::size_t beforeLines = countMeshElementsIf(mesh, isLine); + merged = Compressor::compressLines(mesh); + std::size_t afterLines = countMeshElementsIf(mesh, isLine); + log("Compressed " + std::to_string(beforeLines) + + " -> " + std::to_string(afterLines) + + " lines (merged " + std::to_string(merged) + " segments)", 1); + } log("Recovering original grid size.", 1); reduceGrid(mesh, originalGrid_); diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index bbaffa1..6c25f9f 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -7,12 +7,13 @@ namespace meshlib::meshers { class StaircaseMesher : public MesherBase { public: - StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4); + StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, bool compress = false); virtual ~StaircaseMesher() = default; Mesh mesh() const; private: int decimalPlacesInCollapser_; + bool compress_; Mesh surfaceMesh_; 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/src/utils/Types.h b/src/utils/Types.h index 471ca81..7ac4589 100644 --- a/src/utils/Types.h +++ b/src/utils/Types.h @@ -61,5 +61,14 @@ using HexIds = std::array; using UpdateMap = std::array, 2>, 2>; +// Compressor types +using Sign = int; +using PlanePoint = std::array; +using PlaneLinel = std::pair; +using PlaneSurfel = PlanePoint; +using PlaneSurface = std::pair; +using Contour = std::vector; +using CrossLine = std::pair>; + } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cea5caa..41430ce 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,12 +15,7 @@ include_directories( ) add_executable(tessellator_tests - "app/launcherTest.cpp" - "app/vtkIOTest.cpp" - "core/CollapserTest.cpp" - "core/SlicerTest.cpp" - "core/SnapperTest.cpp" - "core/SmootherTest.cpp" + "core/CompressorTest.cpp" "core/SmootherToolsTest.cpp" "core/StaircaserTest.cpp" "types/MeshTest.cpp" @@ -31,18 +26,29 @@ add_executable(tessellator_tests "utils/GridToolsTest.cpp" "utils/MeshToolsTest.cpp" "utils/RedundancyCleanerTest.cpp" - "meshers/StaircaseMesherTest.cpp" "meshers/OffgridMesherTest.cpp" - "meshers/ConformalMesherTest.cpp" ) target_link_libraries(tessellator_tests tessellator-meshers - tessellator-app GTest::gtest GTest::gtest_main ) +if(VTK_FOUND) + target_sources(tessellator_tests PRIVATE + "app/vtkIOTest.cpp" + "app/launcherTest.cpp" + "core/CollapserTest.cpp" + "core/SlicerTest.cpp" + "core/SnapperTest.cpp" + "core/SmootherTest.cpp" + "meshers/StaircaseMesherTest.cpp" + "meshers/ConformalMesherTest.cpp" + ) + target_link_libraries(tessellator_tests tessellator-app) +endif() + if (TESSELLATOR_ENABLE_CGAL) include_directories( ${PROJECT_SOURCE_DIR}/src/cgal/ diff --git a/test/MeshFixtures.h b/test/MeshFixtures.h index bda4612..8707e29 100644 --- a/test/MeshFixtures.h +++ b/test/MeshFixtures.h @@ -1269,4 +1269,66 @@ static Mesh buildProblematicTriMesh2() } } + +// Helper to add a quad (as a surface with four vertices) to a mesh +static void addQuad(Mesh& mesh, const std::array& v0, const std::array& v1, + const std::array& v2, const std::array& v3) { + if (mesh.groups.empty()) { + mesh.groups.emplace_back(); + } + + auto findOrAddCoord = [&](const std::array& gridIdx) { + double pos[3]; + pos[0] = mesh.grid[0][gridIdx[0]]; + pos[1] = mesh.grid[1][gridIdx[1]]; + pos[2] = mesh.grid[2][gridIdx[2]]; + + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + if (mesh.coordinates[i](0) == pos[0] && + mesh.coordinates[i](1) == pos[1] && + mesh.coordinates[i](2) == pos[2]) { + return i; + } + } + mesh.coordinates.push_back(Coordinate({pos[0], pos[1], pos[2]})); + return static_cast(mesh.coordinates.size() - 1); + }; + + CoordinateId c0 = findOrAddCoord(v0); + CoordinateId c1 = findOrAddCoord(v1); + CoordinateId c2 = findOrAddCoord(v2); + CoordinateId c3 = findOrAddCoord(v3); + + mesh.groups[0].elements.push_back(Element({c0, c1, c2, c3}, Element::Type::Surface)); +} + +// Helper to add a line (as a line with two vertices) to a mesh +static void addLine(Mesh& mesh, const std::array& v0, const std::array& v1) { + if (mesh.groups.empty()) { + mesh.groups.emplace_back(); + } + + auto findOrAddCoord = [&](const std::array& gridIdx) { + double pos[3]; + pos[0] = mesh.grid[0][gridIdx[0]]; + pos[1] = mesh.grid[1][gridIdx[1]]; + pos[2] = mesh.grid[2][gridIdx[2]]; + + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + if (mesh.coordinates[i](0) == pos[0] && + mesh.coordinates[i](1) == pos[1] && + mesh.coordinates[i](2) == pos[2]) { + return i; + } + } + mesh.coordinates.push_back(Coordinate({pos[0], pos[1], pos[2]})); + return static_cast(mesh.coordinates.size() - 1); + }; + + CoordinateId c0 = findOrAddCoord(v0); + CoordinateId c1 = findOrAddCoord(v1); + + mesh.groups[0].elements.push_back(Element({c0, c1}, Element::Type::Line)); +} + } \ No newline at end of file diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 3942428..760a483 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -131,3 +131,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/core/CompressorTest.cpp b/test/core/CompressorTest.cpp new file mode 100644 index 0000000..54c836e --- /dev/null +++ b/test/core/CompressorTest.cpp @@ -0,0 +1,365 @@ +#include +#include "core/Compressor.h" +#include "core/Splitter.h" +#include "MeshFixtures.h" +#include "utils/MeshTools.h" + +using namespace meshlib; +using namespace meshlib::utils::meshTools; + +namespace meshlib::tests { + +class CompressorTest : public ::testing::Test { +protected: + void SetUp() override { + grid_ = { + std::vector{0, 1, 2, 3, 4, 5, 6}, + std::vector{0, 1, 2, 3, 4, 5, 6}, + std::vector{0, 1, 2, 3, 4, 5, 6} + }; + } + + Grid grid_; +}; + +TEST_F(CompressorTest, Compress2x2QuadsIntoOneSurface) { + // Create 4 quads arranged in a 2x2 pattern on the same plane + // They should be merged into a single surface + + Mesh mesh; + mesh.grid = grid_; + + // Quad 1: bottom-left (cells 0,0 to 1,1) + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad 2: bottom-right (cells 1,0 to 2,1) + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + // Quad 3: top-left (cells 0,1 to 1,2) + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + // Quad 4: top-right (cells 1,1 to 2,2) + addQuad(mesh, {1, 1, 0}, {2, 1, 0}, {2, 2, 0}, {1, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 3u); + ASSERT_EQ(mesh.groups.size(), 1u); + ASSERT_EQ(mesh.groups[0].elements.size(), 1u); + + EXPECT_EQ( + CoordinateIds({0, 4, 8, 7}), + mesh.groups[0].elements[0].vertices + ); + +} + +TEST_F(CompressorTest, DoesNotCompressNonCoplanarQuads) { + // Create quads on different planes - should not be merged + + Mesh mesh; + mesh.grid = grid_; + + // Quad on z=0 plane + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad on z=1 plane (different plane) + addQuad(mesh, {0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); +} + +TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { + // Create quads on same plane but not connected - should not be merged + + Mesh mesh; + mesh.grid = grid_; + + // Quad at bottom-left + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad at top-right (disconnected) + addQuad(mesh, {3, 3, 0}, {4, 3, 0}, {4, 4, 0}, {3, 4, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); +} + +TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { + // Create 8 quads forming a ring with a hole in the middle + // The ring decomposes into 4 rectangles (left col, right col, top center, bottom center) + + Mesh mesh; + mesh.grid = grid_; + + // Outer ring of quads (leaving center 1,1 to 2,2 empty) + // Layout: + // x=0 x=1 x=2 x=3 + // y=3 [5] [8] [6] + // y=2 [3] hole [4] + // y=1 [1] [7] [2] + // y=0 + // Bottom row + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); + // Middle row (sides only) + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + addQuad(mesh, {2, 1, 0}, {3, 1, 0}, {3, 2, 0}, {2, 2, 0}); + // Top row + addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0}); + addQuad(mesh, {2, 2, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}); + // Corners to complete the ring + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + addQuad(mesh, {1, 3, 0}, {2, 3, 0}, {2, 2, 0}, {1, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + auto finalCount = countMeshElementsIf(mesh, isQuad); + + // Optimal decomposition: 4 rectangles + // - Left column (quads 1,3,5): cells x=0, y=0-3 + // - Right column (quads 2,4,6): cells x=2-3, y=0-3 + // - Top center (quad 8): cell x=1-2, y=2-3 + // - Bottom center (quad 7): cell x=1-2, y=0-1 + EXPECT_EQ(finalCount, 4u); + EXPECT_EQ(merged, 4u); // 8 - 4 = 4 surfaces merged +} + +TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { + // Create quads with different normal directions - should not be merged + + Mesh mesh; + mesh.grid = grid_; + + // Quad on z=0 plane, normal pointing +z + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad on y=0 plane, normal pointing +y (different orientation) + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 0, 1}, {0, 0, 1}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); +} + +TEST_F(CompressorTest, CompressAndSplit2x2GridRoundTrip) { + // Create 4 quads in 2x2 grid, compress to 1 surface, split back to 4 quads + + Mesh mesh; + mesh.grid = grid_; + + // 2x2 grid of quads + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + addQuad(mesh, {1, 1, 0}, {2, 1, 0}, {2, 2, 0}, {1, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); + + // Compress: 4 quads -> 1 surface + auto merged = core::Compressor::compressSurfaces(mesh); + EXPECT_EQ(merged, 3u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + + // Split: 1 surface -> 4 quads + auto splitCount = core::Splitter::splitSurfaces(mesh); + EXPECT_EQ(splitCount, 4u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); +} + +TEST_F(CompressorTest, CompressAndSplitRingRoundTrip) { + // Create ring of 8 quads, compress, split back + + Mesh mesh; + mesh.grid = grid_; + + // Ring of 8 quads (same as CompressWithHoleCreatesInnerContour) + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + addQuad(mesh, {2, 1, 0}, {3, 1, 0}, {3, 2, 0}, {2, 2, 0}); + addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0}); + addQuad(mesh, {2, 2, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}); + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + addQuad(mesh, {1, 3, 0}, {2, 3, 0}, {2, 2, 0}, {1, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); + + // Compress: 8 quads -> 4 surfaces (left col, right col, top center, bottom center) + auto merged = core::Compressor::compressSurfaces(mesh); + EXPECT_EQ(merged, 4u); // 8 - 4 = 4 surfaces merged + auto compressedCount = countMeshElementsIf(mesh, isQuad); + EXPECT_EQ(compressedCount, 4u); + + // Split: 4 surfaces -> 8 quads + auto splitCount = core::Splitter::splitSurfaces(mesh); + EXPECT_EQ(splitCount, 8u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); +} + +TEST_F(CompressorTest, CompressAndSplit3x3GridRoundTrip) { + // Create 9 quads in 3x3 grid, compress to 1 surface, split back to 9 quads + + Mesh mesh; + mesh.grid = grid_; + + // 3x3 grid of quads + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + addQuad(mesh, + {i, j, 0}, {i+1, j, 0}, + {i+1, j+1, 0}, {i, j+1, 0}); + } + } + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u); + + // Compress: 9 quads -> 1 surface + auto merged = core::Compressor::compressSurfaces(mesh); + EXPECT_EQ(merged, 8u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + + // Split: 1 surface -> 9 quads + auto splitCount = core::Splitter::splitSurfaces(mesh); + EXPECT_EQ(splitCount, 9u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u); +} + +// ============== Line Compression Tests ============== + +TEST_F(CompressorTest, Compress2CollinearLinesIntoOne) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + + ASSERT_EQ(mesh.groups[0].elements.size(), 1u); + const auto& line = mesh.groups[0].elements[0]; + EXPECT_EQ(line.type, Element::Type::Line); + EXPECT_EQ(line.vertices.size(), 2u); +} + +TEST_F(CompressorTest, DoesNotCompressNonCollinearLines) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {0, 0, 0}, {0, 1, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); +} + +TEST_F(CompressorTest, DoesNotCompressDisconnectedLines) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {3, 0, 0}, {4, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); +} + +TEST_F(CompressorTest, Compress3LinesIntoOne) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + addLine(mesh, {2, 0, 0}, {3, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 2u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); +} + +TEST_F(CompressorTest, CompressAndSplit2LineRoundTrip) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + + auto originalLines = countMeshElementsIf(mesh, isLine); + EXPECT_EQ(originalLines, 2u); + + core::Compressor::compressLines(mesh); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + + auto splitCount = core::Splitter::splitLines(mesh); + + EXPECT_EQ(splitCount, 2u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); +} + +TEST_F(CompressorTest, CompressAndSplit5LineRoundTrip) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + addLine(mesh, {2, 0, 0}, {3, 0, 0}); + addLine(mesh, {3, 0, 0}, {4, 0, 0}); + addLine(mesh, {4, 0, 0}, {5, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 5u); + + core::Compressor::compressLines(mesh); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + + auto splitCount = core::Splitter::splitLines(mesh); + + EXPECT_EQ(splitCount, 5u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 5u); +} + +TEST_F(CompressorTest, CompressMixedDirections) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + addLine(mesh, {0, 0, 0}, {0, 1, 0}); + addLine(mesh, {0, 1, 0}, {0, 2, 0}); + addLine(mesh, {0, 0, 0}, {0, 0, 1}); + addLine(mesh, {0, 0, 1}, {0, 0, 2}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 6u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 3u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); +} + +} diff --git a/test/core/StaircaserTest.cpp b/test/core/StaircaserTest.cpp index 4975e5f..baa63b1 100644 --- a/test/core/StaircaserTest.cpp +++ b/test/core/StaircaserTest.cpp @@ -2204,7 +2204,6 @@ TEST_F(StaircaserTest, selectiveStructurerWithEmptySetOfCells) } - TEST_F(StaircaserTest, modifyCoordinateOfASpecificCell) { 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/alhambra/alhambra.conformal.tessellator.json b/testData/cases/alhambra/alhambra.conformal.tessellator.json index f2b6c20..9f17e14 100644 --- a/testData/cases/alhambra/alhambra.conformal.tessellator.json +++ b/testData/cases/alhambra/alhambra.conformal.tessellator.json @@ -11,7 +11,8 @@ "type" : "conformal", "options" : { "edgePoints" : 6, - "forbiddenLength" : 0.15 + "forbiddenLength" : 0.15, + "compress": false } } } \ No newline at end of file diff --git a/testData/cases/alhambra/alhambra.tessellator.json b/testData/cases/alhambra/alhambra.tessellator.json index 2519ae2..f1d9032 100644 --- a/testData/cases/alhambra/alhambra.tessellator.json +++ b/testData/cases/alhambra/alhambra.tessellator.json @@ -6,5 +6,11 @@ [ 60.0, 60.0, 10.0] ] }, - "object": {"filename": "alhambra.stl"} + "object": {"filename": "alhambra.stl"}, + "mesher": { + "type" : "staircase", + "options" : { + "compress": true + } + } } \ No newline at end of file