diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 73d442b..6e1b78f 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: [
@@ -51,7 +56,19 @@ jobs:
- name: Windows configure and build
if: matrix.preset.name=='msbuild'
run: |
- cmake --preset ${{matrix.preset.name}} -S . -B build
+ $configured = $false
+ for ($attempt = 1; $attempt -le 3; $attempt++) {
+ cmake --preset ${{matrix.preset.name}} -S . -B build
+ if ($LASTEXITCODE -eq 0) {
+ $configured = $true
+ break
+ }
+ if ($attempt -lt 3) {
+ Write-Host "Configure failed (attempt $attempt/3). Retrying in 20 seconds..."
+ Start-Sleep -Seconds 20
+ }
+ }
+ if (-not $configured) { exit 1 }
cmake --build build --config ${{matrix.build-type}} -j
- name: Windows Run tests
@@ -61,7 +78,15 @@ jobs:
- name: Ubuntu configure and build
if: matrix.preset.name=='gnu'
run: |
- cmake --preset ${{matrix.preset.name}} -S . -B build
+ configured=0
+ for attempt in 1 2 3; do
+ cmake --preset ${{matrix.preset.name}} -S . -B build && configured=1 && break
+ if [ "$attempt" -lt 3 ]; then
+ echo "Configure failed (attempt $attempt/3). Retrying in 20 seconds..."
+ sleep 20
+ fi
+ done
+ [ "$configured" -eq 1 ] || exit 1
cmake --build build -j
- name: Ubuntu Run tests
@@ -69,4 +94,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/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..c2d357f 100644
--- a/src/app/launcher.cpp
+++ b/src/app/launcher.cpp
@@ -109,11 +109,40 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::s
}
return res;
}
+
+bool readStaircaseMesherCompressOption(const std::string &fn)
+{
+ nlohmann::json j;
+ {
+ std::ifstream i(fn);
+ i >> j;
+ }
+ if (j["mesher"].contains("options") &&
+ j["mesher"]["options"].contains("compress")) {
+ return j["mesher"]["options"]["compress"];
+ }
+ return false;
+}
+
+bool readExportGridOption(const std::string &fn)
+{
+ nlohmann::json j;
+ {
+ std::ifstream i(fn);
+ i >> j;
+ }
+ if (j["mesher"].contains("options") &&
+ j["mesher"]["options"].contains("exportGrid")) {
+ return j["mesher"]["options"]["exportGrid"];
+ }
+ return true;
+}
std::unique_ptr buildMesher(const Mesh &in, const std::string &fn)
{
auto mesherType = readMesherType(fn);
if (mesherType == meshlib::app::staircase_mesher) {
- return std::make_unique(meshlib::meshers::StaircaseMesher{in});
+ bool compress = readStaircaseMesherCompressOption(fn);
+ 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)});
} else {
@@ -154,7 +183,9 @@ int launcher(int argc, const char* argv[])
auto extension = readExtension(inputFilename);
exportMeshToVTU(outputFolder / (basename + ".tessellator." + extension + ".vtk"), resultMesh);
- exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), resultMesh.grid);
+ if (readExportGridOption(inputFilename)) {
+ exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), resultMesh.grid);
+ }
return EXIT_SUCCESS;
}
diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp
index 9457e14..f1e6c2e 100644
--- a/src/app/vtkIO.cpp
+++ b/src/app/vtkIO.cpp
@@ -1,6 +1,5 @@
#include "vtkIO.h"
-#include
#include
#include
#include
@@ -43,7 +42,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);
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/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..dd9f73f 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,28 @@ 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"
+ "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/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/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
| | | | | | |