From cb75597758e425c018d3829f4deb87722c02b590 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 8 May 2026 11:41:09 +0200 Subject: [PATCH 01/13] Add compressor functionality to the tessellator - Implemented Compressor class for merging coplanar quad surfaces. - Updated StaircaseMesher to include compression option. - Enhanced launcher to read compression settings from JSON input. - Added tests for Compressor functionality. --- src/app/launcher.cpp | 17 +- src/core/CMakeLists.txt | 1 + src/core/Compressor.cpp | 604 ++++++++++++++++++++++++++++++++ src/core/Compressor.h | 75 ++++ src/meshers/StaircaseMesher.cpp | 16 +- src/meshers/StaircaseMesher.h | 3 +- src/utils/Types.h | 9 + test/CMakeLists.txt | 1 + test/core/CompressorTest.cpp | 151 ++++++++ 9 files changed, 873 insertions(+), 4 deletions(-) create mode 100644 src/core/Compressor.cpp create mode 100644 src/core/Compressor.h create mode 100644 test/core/CompressorTest.cpp diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index b8dd444..f530778 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -109,11 +109,26 @@ 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; +} 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 { diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d481f43..faab34a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -2,6 +2,7 @@ message(STATUS "Creating build system for tessellator-core") add_library(tessellator-core "Collapser.cpp" + "Compressor.cpp" "Slicer.cpp" "Snapper.cpp" "Smoother.cpp" diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp new file mode 100644 index 0000000..2e09033 --- /dev/null +++ b/src/core/Compressor.cpp @@ -0,0 +1,604 @@ +#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(); + + // Replace surface elements + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + if (mesh.groups[g].elements[e].type == Element::Type::Surface) { + auto it = std::find(surfIndices.begin(), surfIndices.end(), e); + if (it != surfIndices.end()) { + std::size_t idx = std::distance(surfIndices.begin(), it); + if (idx < compressedSurfs.size()) { + mesh.groups[g].elements[e] = compressedSurfs[idx]; + } + } + } + } + } + + return totalOriginal - totalCompressed; +} + +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); + for (CellDir i = ext.first[0]; i < ext.second[0]; i++) { + for (CellDir j = ext.first[1]; j < ext.second[1]; 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..cfae369 --- /dev/null +++ b/src/core/Compressor.h @@ -0,0 +1,75 @@ +#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); + +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); + + // 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/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 7ad6458..0a88a6f 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); @@ -66,6 +68,16 @@ 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("Removing repeated and overlapping elements.", 1); RedundancyCleaner::removeOverlappedElementsByDimension(mesh, dimensions); 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..d345ee2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(tessellator_tests "app/launcherTest.cpp" "app/vtkIOTest.cpp" "core/CollapserTest.cpp" + "core/CompressorTest.cpp" "core/SlicerTest.cpp" "core/SnapperTest.cpp" "core/SmootherTest.cpp" diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp new file mode 100644 index 0000000..a4aa919 --- /dev/null +++ b/test/core/CompressorTest.cpp @@ -0,0 +1,151 @@ +#include +#include "core/Compressor.h" +#include "MeshFixtures.h" + +namespace meshlib::tests { + +class CompressorTest : public ::testing::Test { +protected: + void SetUp() override { + grid_ = { + std::vector{0, 1, 2, 3, 4}, + std::vector{0, 1, 2, 3, 4}, + std::vector{0, 1, 2, 3, 4} + }; + } + + 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, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); +} + +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, CompressLShapeIntoOneSurface) { + // Create 3 quads in L-shape - should be merged into one surface + + Mesh mesh; + mesh.grid = grid_; + + // Quad 1: bottom-left + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad 2: bottom-right + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + // Quad 3: top-left (forming L-shape) + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 3u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); +} + +TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { + // Create 8 quads forming a ring with a hole in the middle + // Should be merged into one surface with inner contour + + Mesh mesh; + mesh.grid = grid_; + + // Outer ring of quads (leaving center 1,1 to 2,2 empty) + // 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); + + EXPECT_EQ(merged, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); +} + +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); +} + +} From debed931896ac23ebdc73ce576834cb621d2de9d Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Tue, 12 May 2026 11:33:02 +0200 Subject: [PATCH 02/13] Fix compressor test crash and element removal - Add groups initialization check in addQuad() helper to prevent segfault - Fix compressSurfaces() to properly remove merged elements - Update test expectations for merged return value --- CMakePresets.json | 6 +++++- src/app/CMakeLists.txt | 37 ++++++++++++++++++++---------------- src/app/vtkIO.cpp | 3 +-- src/core/Compressor.cpp | 16 +++++++++------- test/CMakeLists.txt | 22 ++++++++++++--------- test/MeshFixtures.h | 36 +++++++++++++++++++++++++++++++++++ test/core/CompressorTest.cpp | 10 +++++++--- 7 files changed, 92 insertions(+), 38 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 322d7c9..90993b7 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -26,7 +26,11 @@ "name": "gnu", "displayName": "GNU g++ compiler", "generator": "Ninja", - "inherits": "default" + "inherits": "default", + "cacheVariables": { + "TESSELLATOR_ENABLE_TESTS": "ON", + "TESSELLATOR_ENABLE_CGAL": "OFF" + } }, { "name": "docker", 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/vtkIO.cpp b/src/app/vtkIO.cpp index 9457e14..4565d89 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/Compressor.cpp b/src/core/Compressor.cpp index 2e09033..82192bd 100644 --- a/src/core/Compressor.cpp +++ b/src/core/Compressor.cpp @@ -40,18 +40,20 @@ std::size_t Compressor::compressSurfaces(Mesh& mesh) { std::vector compressedSurfs = compressSurfs_(mesh.coordinates, surfs); totalCompressed += compressedSurfs.size(); - // Replace surface elements + // 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) { - auto it = std::find(surfIndices.begin(), surfIndices.end(), e); - if (it != surfIndices.end()) { - std::size_t idx = std::distance(surfIndices.begin(), it); - if (idx < compressedSurfs.size()) { - mesh.groups[g].elements[e] = compressedSurfs[idx]; - } + 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; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d345ee2..dd9f73f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,13 +15,7 @@ include_directories( ) add_executable(tessellator_tests - "app/launcherTest.cpp" - "app/vtkIOTest.cpp" - "core/CollapserTest.cpp" "core/CompressorTest.cpp" - "core/SlicerTest.cpp" - "core/SnapperTest.cpp" - "core/SmootherTest.cpp" "core/SmootherToolsTest.cpp" "core/StaircaserTest.cpp" "types/MeshTest.cpp" @@ -32,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..b23fe42 100644 --- a/test/MeshFixtures.h +++ b/test/MeshFixtures.h @@ -1269,4 +1269,40 @@ 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) { + // Ensure at least one group exists + if (mesh.groups.empty()) { + mesh.groups.emplace_back(); + } + + // Find or create coordinates + 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); + + // Add quad as a surface with four vertices + mesh.groups[0].elements.push_back(Element({c0, c1, c2, c3}, Element::Type::Surface)); +} + } \ No newline at end of file diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index a4aa919..86d2411 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -1,6 +1,10 @@ #include #include "core/Compressor.h" #include "MeshFixtures.h" +#include "utils/MeshTools.h" + +using namespace meshlib; +using namespace meshlib::utils::meshTools; namespace meshlib::tests { @@ -37,7 +41,7 @@ TEST_F(CompressorTest, Compress2x2QuadsIntoOneSurface) { auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 1u); + EXPECT_EQ(merged, 3u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); } @@ -96,7 +100,7 @@ TEST_F(CompressorTest, CompressLShapeIntoOneSurface) { auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 1u); + EXPECT_EQ(merged, 2u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); } @@ -125,7 +129,7 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 1u); + EXPECT_EQ(merged, 7u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); } From 4436d654607b886bb3ab0872f60ecfac23479655 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 09:09:46 +0200 Subject: [PATCH 03/13] Adds IDE configuration files --- .vscode/launch.json | 35 +++++ CMakePresets.json | 9 ++ resources/Eigen.natvis | 253 ++++++++++++++++++++++++++++++ resources/nlohmann_json.natvis | 278 +++++++++++++++++++++++++++++++++ 4 files changed, 575 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 resources/Eigen.natvis create mode 100644 resources/nlohmann_json.natvis diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..17c6a06 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "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 90993b7..5be2e29 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -32,6 +32,15 @@ "TESSELLATOR_ENABLE_CGAL": "OFF" } }, + { + "name": "gnu-dbg", + "displayName": "GNU g++ compiler - Debug", + "inherits": "gnu", + "binaryDir": "build-dbg/", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, { "name": "docker", "displayName": "Docker (system libraries)", 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 + + + + From e2722ecf59d8fd33b1d249404a069b267c863586 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 11:18:19 +0200 Subject: [PATCH 04/13] Compressor not working --- .gitignore | 3 +- .vscode/launch.json | 30 +++++++++++++++++++ src/app/vtkIO.cpp | 2 +- test/core/CompressorTest.cpp | 26 ++-------------- .../alhambra.conformal.tessellator.json | 3 +- .../cases/alhambra/alhambra.tessellator.json | 8 ++++- 6 files changed, 44 insertions(+), 28 deletions(-) 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 index 17c6a06..542abba 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,36 @@ { "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", diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index 4565d89..f1e6c2e 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -42,7 +42,7 @@ vtkSmartPointer readAsVTU(const std::filesystem::path& file } vtkSmartPointer vtu; - std::string extension = fn.substr(fn.find_last_of(".")).empty() ? "" : "." + fn.substr(fn.find_last_of(".")); + 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/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index 86d2411..3b5affe 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -83,27 +83,6 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); } -TEST_F(CompressorTest, CompressLShapeIntoOneSurface) { - // Create 3 quads in L-shape - should be merged into one surface - - Mesh mesh; - mesh.grid = grid_; - - // Quad 1: bottom-left - addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); - // Quad 2: bottom-right - addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); - // Quad 3: top-left (forming L-shape) - addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); - - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 3u); - - auto merged = core::Compressor::compressSurfaces(mesh); - - EXPECT_EQ(merged, 2u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); -} - TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { // Create 8 quads forming a ring with a hole in the middle // Should be merged into one surface with inner contour @@ -128,9 +107,8 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); auto merged = core::Compressor::compressSurfaces(mesh); - - EXPECT_EQ(merged, 7u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); } TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { 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 From b2c211a4edd6ddd1256d56affef60e725b1d6f28 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 11:40:08 +0200 Subject: [PATCH 05/13] Iterating --- src/meshers/StaircaseMesher.cpp | 12 ++++++------ test/core/CompressorTest.cpp | 9 ++++++++- test/core/StaircaserTest.cpp | 1 - 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 0a88a6f..059f8ec 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -68,6 +68,12 @@ void StaircaseMesher::process(Mesh& mesh) const logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); logNumberOfLines(countMeshElementsIf(mesh, isLine)); + log("Removing repeated and overlapping elements.", 1); + RedundancyCleaner::removeOverlappedElementsByDimension(mesh, dimensions); + + logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); + logNumberOfLines(countMeshElementsIf(mesh, isLine)); + if (compress_) { log("Compressing surfaces.", 1); std::size_t beforeQuads = countMeshElementsIf(mesh, isQuad); @@ -77,12 +83,6 @@ void StaircaseMesher::process(Mesh& mesh) const " -> " + std::to_string(afterQuads) + " quads (merged " + std::to_string(merged) + " surfaces)", 1); } - - log("Removing repeated and overlapping elements.", 1); - RedundancyCleaner::removeOverlappedElementsByDimension(mesh, dimensions); - - logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); - logNumberOfLines(countMeshElementsIf(mesh, isLine)); log("Recovering original grid size.", 1); reduceGrid(mesh, originalGrid_); diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index 3b5affe..e6bb881 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -42,7 +42,14 @@ TEST_F(CompressorTest, Compress2x2QuadsIntoOneSurface) { auto merged = core::Compressor::compressSurfaces(mesh); EXPECT_EQ(merged, 3u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + 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) { 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) { From 683a3921b5a78169df57694b9d7081bf75643342 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 15:05:59 +0200 Subject: [PATCH 06/13] More tests and splitter --- src/core/CMakeLists.txt | 1 + src/core/Splitter.cpp | 182 +++++++++++++++++++++++++++++++++++ src/core/Splitter.h | 42 ++++++++ test/core/CompressorTest.cpp | 111 ++++++++++++++++++++- 4 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 src/core/Splitter.cpp create mode 100644 src/core/Splitter.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index faab34a..142beee 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(tessellator-core "Snapper.cpp" "Smoother.cpp" "SmootherTools.cpp" + "Splitter.cpp" "Staircaser.cpp" ) diff --git a/src/core/Splitter.cpp b/src/core/Splitter.cpp new file mode 100644 index 0000000..b25ac2e --- /dev/null +++ b/src/core/Splitter.cpp @@ -0,0 +1,182 @@ +#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::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..f063765 --- /dev/null +++ b/src/core/Splitter.h @@ -0,0 +1,42 @@ +#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); + +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); +}; + +} diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index e6bb881..b87cfa4 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -1,5 +1,6 @@ #include #include "core/Compressor.h" +#include "core/Splitter.h" #include "MeshFixtures.h" #include "utils/MeshTools.h" @@ -92,12 +93,18 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { // Create 8 quads forming a ring with a hole in the middle - // Should be merged into one surface with inner contour + // The ring decomposes into 3 rectangles (left col, right col, center cols) 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}); @@ -115,7 +122,14 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); + auto finalCount = countMeshElementsIf(mesh, isQuad); + + // Optimal decomposition: 3 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 + // - Center (quads 7,8): cells x=1-2, y=0 and y=2-3 + EXPECT_EQ(finalCount, 3u); + EXPECT_EQ(merged, 5u); // 8 - 3 = 5 surfaces merged } TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { @@ -137,4 +151,97 @@ TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { 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); + + // Debug: print compressed surface + std::cerr << "Compressed surface vertices: "; + for (auto vid : mesh.groups[0].elements[0].vertices) { + std::cerr << "(" << mesh.coordinates[vid](0) << "," + << mesh.coordinates[vid](1) << "," + << mesh.coordinates[vid](2) << ") "; + } + std::cerr << std::endl; + + // Split: 1 surface -> 4 quads + auto splitCount = core::Splitter::splitSurfaces(mesh); + std::cout << "Split count: " << splitCount << std::endl; + 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 -> 3 surfaces + auto merged = core::Compressor::compressSurfaces(mesh); + EXPECT_EQ(merged, 5u); + auto compressedCount = countMeshElementsIf(mesh, isQuad); + EXPECT_EQ(compressedCount, 3u); + + // Split: 3 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); +} + } From 073af57127e55a18bfd6ca18f6bce413d8bdf1f2 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 19:04:14 +0200 Subject: [PATCH 07/13] tesrs pass --- src/core/Compressor.cpp | 8 ++++++-- test/core/CompressorTest.cpp | 29 ++++++++++------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp index 82192bd..0255ad8 100644 --- a/src/core/Compressor.cpp +++ b/src/core/Compressor.cpp @@ -185,8 +185,12 @@ std::vector Compressor::compressSurf_( utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d1); ext.second[1] = utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d2); - for (CellDir i = ext.first[0]; i < ext.second[0]; i++) { - for (CellDir j = ext.first[1]; j < ext.second[1]; j++) { + 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); } diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index b87cfa4..e011b4f 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -93,7 +93,7 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { // Create 8 quads forming a ring with a hole in the middle - // The ring decomposes into 3 rectangles (left col, right col, center cols) + // The ring decomposes into 4 rectangles (left col, right col, top center, bottom center) Mesh mesh; mesh.grid = grid_; @@ -124,12 +124,13 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { auto finalCount = countMeshElementsIf(mesh, isQuad); - // Optimal decomposition: 3 rectangles + // 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 - // - Center (quads 7,8): cells x=1-2, y=0 and y=2-3 - EXPECT_EQ(finalCount, 3u); - EXPECT_EQ(merged, 5u); // 8 - 3 = 5 surfaces merged + // - 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) { @@ -170,18 +171,8 @@ TEST_F(CompressorTest, CompressAndSplit2x2GridRoundTrip) { EXPECT_EQ(merged, 3u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); - // Debug: print compressed surface - std::cerr << "Compressed surface vertices: "; - for (auto vid : mesh.groups[0].elements[0].vertices) { - std::cerr << "(" << mesh.coordinates[vid](0) << "," - << mesh.coordinates[vid](1) << "," - << mesh.coordinates[vid](2) << ") "; - } - std::cerr << std::endl; - // Split: 1 surface -> 4 quads auto splitCount = core::Splitter::splitSurfaces(mesh); - std::cout << "Split count: " << splitCount << std::endl; EXPECT_EQ(splitCount, 4u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); } @@ -204,13 +195,13 @@ TEST_F(CompressorTest, CompressAndSplitRingRoundTrip) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); - // Compress: 8 quads -> 3 surfaces + // Compress: 8 quads -> 4 surfaces (left col, right col, top center, bottom center) auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 5u); + EXPECT_EQ(merged, 4u); // 8 - 4 = 4 surfaces merged auto compressedCount = countMeshElementsIf(mesh, isQuad); - EXPECT_EQ(compressedCount, 3u); + EXPECT_EQ(compressedCount, 4u); - // Split: 3 surfaces -> 8 quads + // Split: 4 surfaces -> 8 quads auto splitCount = core::Splitter::splitSurfaces(mesh); EXPECT_EQ(splitCount, 8u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); From a8a2c92e05cdf5c311df233b38a1a6defa49e046 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 21:03:42 +0200 Subject: [PATCH 08/13] Surface compressor works! --- src/core/Compressor.cpp | 147 ++++++++++++++++++++++++++++++++++++++++ src/core/Compressor.h | 15 ++++ src/core/Splitter.h | 30 ++++++++ 3 files changed, 192 insertions(+) diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp index 0255ad8..71c0f33 100644 --- a/src/core/Compressor.cpp +++ b/src/core/Compressor.cpp @@ -59,6 +59,153 @@ std::size_t Compressor::compressSurfaces(Mesh& mesh) { 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) { diff --git a/src/core/Compressor.h b/src/core/Compressor.h index cfae369..5b66e32 100644 --- a/src/core/Compressor.h +++ b/src/core/Compressor.h @@ -14,6 +14,10 @@ class Compressor { // 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_( @@ -32,6 +36,17 @@ class Compressor { 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); diff --git a/src/core/Splitter.h b/src/core/Splitter.h index f063765..91206ef 100644 --- a/src/core/Splitter.h +++ b/src/core/Splitter.h @@ -11,6 +11,10 @@ class Splitter { // 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_( @@ -37,6 +41,32 @@ class Splitter { 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); }; } From a6eeabb0f1c51f7fda81d77dccbbceaa88090e97 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Sat, 16 May 2026 07:35:27 +0200 Subject: [PATCH 09/13] Adds compress lines --- src/app/launcher.cpp | 17 +++- src/core/Splitter.cpp | 147 ++++++++++++++++++++++++++++++++ src/meshers/StaircaseMesher.cpp | 15 +++- src/meshers/StaircaseMesher.h | 3 +- test/MeshFixtures.h | 32 ++++++- test/core/CompressorTest.cpp | 133 ++++++++++++++++++++++++++++- 6 files changed, 337 insertions(+), 10 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index f530778..a6ce9f6 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -123,12 +123,27 @@ bool readStaircaseMesherCompressOption(const std::string &fn) } return false; } + +bool readStaircaseMesherCompressLinesOption(const std::string &fn) +{ + nlohmann::json j; + { + std::ifstream i(fn); + i >> j; + } + if (j["mesher"].contains("options") && + j["mesher"]["options"].contains("compressLines")) { + return j["mesher"]["options"]["compressLines"]; + } + return false; +} std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) { auto mesherType = readMesherType(fn); if (mesherType == meshlib::app::staircase_mesher) { bool compress = readStaircaseMesherCompressOption(fn); - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, compress}); + bool compressLines = readStaircaseMesherCompressLinesOption(fn); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, compress, compressLines}); } else if (mesherType == meshlib::app::conformal_mesher) { return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); } else { diff --git a/src/core/Splitter.cpp b/src/core/Splitter.cpp index b25ac2e..059b5a7 100644 --- a/src/core/Splitter.cpp +++ b/src/core/Splitter.cpp @@ -44,6 +44,153 @@ std::size_t Splitter::splitSurfaces(Mesh& mesh) { 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, diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 059f8ec..abee5cd 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -18,10 +18,11 @@ using namespace utils; using namespace core; using namespace meshTools; -StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, bool compress) : +StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, bool compress, bool compressLines) : MesherBase(inputMesh), decimalPlacesInCollapser_(decimalPlacesInCollapser), - compress_(compress) + compress_(compress), + compressLines_(compressLines) { log("Preparing surfaces."); surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -84,6 +85,16 @@ void StaircaseMesher::process(Mesh& mesh) const " quads (merged " + std::to_string(merged) + " surfaces)", 1); } + if (compressLines_) { + log("Compressing lines.", 1); + std::size_t beforeLines = countMeshElementsIf(mesh, isLine); + std::size_t 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 6c25f9f..0f91ea1 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -7,13 +7,14 @@ namespace meshlib::meshers { class StaircaseMesher : public MesherBase { public: - StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, bool compress = false); + StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, bool compress = false, bool compressLines = false); virtual ~StaircaseMesher() = default; Mesh mesh() const; private: int decimalPlacesInCollapser_; bool compress_; + bool compressLines_; Mesh surfaceMesh_; diff --git a/test/MeshFixtures.h b/test/MeshFixtures.h index b23fe42..8707e29 100644 --- a/test/MeshFixtures.h +++ b/test/MeshFixtures.h @@ -1273,12 +1273,10 @@ 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) { - // Ensure at least one group exists if (mesh.groups.empty()) { mesh.groups.emplace_back(); } - // Find or create coordinates auto findOrAddCoord = [&](const std::array& gridIdx) { double pos[3]; pos[0] = mesh.grid[0][gridIdx[0]]; @@ -1301,8 +1299,36 @@ static void addQuad(Mesh& mesh, const std::array& v0, 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 index e011b4f..54c836e 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -13,9 +13,9 @@ class CompressorTest : public ::testing::Test { protected: void SetUp() override { grid_ = { - std::vector{0, 1, 2, 3, 4}, - std::vector{0, 1, 2, 3, 4}, - std::vector{0, 1, 2, 3, 4} + 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} }; } @@ -235,4 +235,131 @@ TEST_F(CompressorTest, CompressAndSplit3x3GridRoundTrip) { 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); +} + } From f95e55f5987172c5ae3f6ad4cb18586634c79ea9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 10:44:56 +0000 Subject: [PATCH 10/13] ci: disable vcpkg GHA binary source in build workflow Agent-Logs-Url: https://github.com/OpenSEMBA/tessellator/sessions/2ab9c1cf-5df7-41b4-aa40-c9d80938a5af Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- .github/workflows/build-and-test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 73d442b..3d86283 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -12,6 +12,8 @@ concurrency: jobs: builds-and-tests: + env: + VCPKG_BINARY_SOURCES: clear strategy: matrix: preset: [ @@ -69,4 +71,4 @@ jobs: run: build/bin/tessellator_tests - \ No newline at end of file + From 1742a944e015a9b0ee49a05bdba16d46cec73dd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 10:45:38 +0000 Subject: [PATCH 11/13] ci: scope workflow token permissions Agent-Logs-Url: https://github.com/OpenSEMBA/tessellator/sessions/2ab9c1cf-5df7-41b4-aa40-c9d80938a5af Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- .github/workflows/build-and-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 3d86283..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 }} From 5ec82012ea13ee470bb2d6b758f372c4af628b67 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Sun, 17 May 2026 11:11:14 +0200 Subject: [PATCH 12/13] Adds comoressor abf exportGrid options --- src/app/launcher.cpp | 15 ++++++++------- src/meshers/StaircaseMesher.cpp | 11 ++++------- src/meshers/StaircaseMesher.h | 3 +-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index a6ce9f6..c2d357f 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -124,7 +124,7 @@ bool readStaircaseMesherCompressOption(const std::string &fn) return false; } -bool readStaircaseMesherCompressLinesOption(const std::string &fn) +bool readExportGridOption(const std::string &fn) { nlohmann::json j; { @@ -132,18 +132,17 @@ bool readStaircaseMesherCompressLinesOption(const std::string &fn) i >> j; } if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("compressLines")) { - return j["mesher"]["options"]["compressLines"]; + j["mesher"]["options"].contains("exportGrid")) { + return j["mesher"]["options"]["exportGrid"]; } - return false; + return true; } std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) { auto mesherType = readMesherType(fn); if (mesherType == meshlib::app::staircase_mesher) { bool compress = readStaircaseMesherCompressOption(fn); - bool compressLines = readStaircaseMesherCompressLinesOption(fn); - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, compress, compressLines}); + 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 { @@ -184,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/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index abee5cd..c0aac52 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -18,11 +18,10 @@ using namespace utils; using namespace core; using namespace meshTools; -StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, bool compress, bool compressLines) : +StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, bool compress) : MesherBase(inputMesh), decimalPlacesInCollapser_(decimalPlacesInCollapser), - compress_(compress), - compressLines_(compressLines) + compress_(compress) { log("Preparing surfaces."); surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -83,12 +82,10 @@ void StaircaseMesher::process(Mesh& mesh) const log("Compressed " + std::to_string(beforeQuads) + " -> " + std::to_string(afterQuads) + " quads (merged " + std::to_string(merged) + " surfaces)", 1); - } - - if (compressLines_) { + log("Compressing lines.", 1); std::size_t beforeLines = countMeshElementsIf(mesh, isLine); - std::size_t merged = Compressor::compressLines(mesh); + merged = Compressor::compressLines(mesh); std::size_t afterLines = countMeshElementsIf(mesh, isLine); log("Compressed " + std::to_string(beforeLines) + " -> " + std::to_string(afterLines) + diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index 0f91ea1..6c25f9f 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -7,14 +7,13 @@ namespace meshlib::meshers { class StaircaseMesher : public MesherBase { public: - StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, bool compress = false, bool compressLines = false); + StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, bool compress = false); virtual ~StaircaseMesher() = default; Mesh mesh() const; private: int decimalPlacesInCollapser_; bool compress_; - bool compressLines_; Mesh surfaceMesh_; From b3b9b6338caa46ed6b409a9f9e02b2e0dedd46dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 11:13:19 +0000 Subject: [PATCH 13/13] ci: retry configure step on transient vcpkg download failures Agent-Logs-Url: https://github.com/OpenSEMBA/tessellator/sessions/e5974172-7eed-4d3b-a308-0d68b508b8c4 Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- .github/workflows/build-and-test.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0123120..6e1b78f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -56,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 @@ -66,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