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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 5fa5556b87e4901e3f1538ef2ed0d01e75aa2e12 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Sun, 17 May 2026 20:09:40 +0200 Subject: [PATCH 13/15] AI assisted implementation of multiobject meshing --- src/app/launcher.cpp | 168 +++++++++++++++++++++++++++-------- src/app/launcher.h | 12 +++ src/app/vtkIO.cpp | 15 ++++ src/types/Mesh.h | 8 +- src/utils/MeshTools.cpp | 54 +++++++++++ src/utils/MeshTools.h | 2 + test/CMakeLists.txt | 1 + test/app/launcherTest.cpp | 75 ++++++++++++++++ test/utils/MeshToolsTest.cpp | 86 ++++++++++++++++++ 9 files changed, 384 insertions(+), 37 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index c2d357f..7373c5c 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -4,6 +4,7 @@ #include "meshers/StaircaseMesher.h" #include "meshers/ConformalMesher.h" #include "utils/GridTools.h" +#include "utils/MeshTools.h" #include #include @@ -13,6 +14,8 @@ #include #include #include +#include +#include namespace meshlib::app { @@ -44,18 +47,51 @@ Grid parseGridFromJSON(const nlohmann::json &j) } } -Mesh readMesh(const std::string &fn) +std::vector readObjectsFromJSON(const std::string& fn) { nlohmann::json j; + { + std::ifstream i(fn); + i >> j; + } + + std::vector objects; + + if (j.contains("objects")) { + for (const auto& obj : j["objects"]) { + ObjectDefinition objDef; + objDef.filename = obj["filename"]; + objDef.group = obj.value("group", std::filesystem::path(obj["filename"]).stem().string()); + if (obj.contains("mesher")) { + objDef.mesherOverride = obj["mesher"]; + } + objects.push_back(objDef); + } + } else if (j.contains("object")) { + ObjectDefinition objDef; + objDef.filename = j["object"]["filename"]; + objDef.group = std::filesystem::path(j["object"]["filename"]).stem().string(); + if (j.contains("mesher")) { + objDef.mesherOverride = j["mesher"]; + } + objects.push_back(objDef); + } else { + throw std::runtime_error("No objects defined in input file"); + } + + return objects; +} +Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) +{ + nlohmann::json j; { std::ifstream i(fn); i >> j; } std::filesystem::path caseFolder = std::filesystem::path(fn).parent_path(); - std::filesystem::path objPathFromInput = j["object"]["filename"]; - std::filesystem::path meshObjectPath = caseFolder / objPathFromInput; + std::filesystem::path meshObjectPath = caseFolder / objDef.filename; std::cout << "-- Reading mesh groups from: " << meshObjectPath; Mesh res = vtkIO::readInputMesh(meshObjectPath); @@ -65,27 +101,48 @@ Mesh readMesh(const std::string &fn) res.grid = parseGridFromJSON(j["grid"]); std::cout << "....... [OK]" << std::endl; + if (res.groups.empty()) { + res.groups.push_back(Group{objDef.group, {}}); + } else { + res = utils::meshTools::extractGroupsByName(res, {objDef.group}); + if (res.groups.empty()) { + res.groups.push_back(Group{objDef.group, {}}); + } else { + res.groups[0].name = objDef.group; + } + } + return res; } -std::string readMesherType(const std::string &fn) +std::string readMesherType(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } - if (j["mesher"].contains("type")) { - return j["mesher"]["type"]; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } else { + return meshlib::app::staircase_mesher; + } + + if (mesherConfig.contains("type")) { + return mesherConfig["type"]; } else { return meshlib::app::staircase_mesher; } } -std::string readExtension(const std::string &fn) +std::string readExtension(const std::string& fn, const std::optional& override) { - auto mesherType = readMesherType(fn); + auto mesherType = readMesherType(fn, override); if (mesherType == meshlib::app::staircase_mesher) { return "str"; } else if (mesherType == meshlib::app::conformal_mesher) { @@ -95,56 +152,81 @@ std::string readExtension(const std::string &fn) } } -meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string &fn) +meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + meshlib::meshers::ConformalMesherOptions res; - if (j["mesher"].contains("options")) { - res.snapperOptions.edgePoints = j["mesher"]["options"]["edgePoints"]; - res.snapperOptions.forbiddenLength = j["mesher"]["options"]["forbiddenLength"]; + if (mesherConfig.contains("options")) { + res.snapperOptions.edgePoints = mesherConfig["options"]["edgePoints"]; + res.snapperOptions.forbiddenLength = mesherConfig["options"]["forbiddenLength"]; } return res; } -bool readStaircaseMesherCompressOption(const std::string &fn) +bool readStaircaseMesherCompressOption(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("compress")) { - return j["mesher"]["options"]["compress"]; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("compress")) { + return mesherConfig["options"]["compress"]; } return false; } -bool readExportGridOption(const std::string &fn) +bool readExportGridOption(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("exportGrid")) { - return j["mesher"]["options"]["exportGrid"]; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("exportGrid")) { + return mesherConfig["options"]["exportGrid"]; } return true; } -std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) + +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const std::optional& override) { - auto mesherType = readMesherType(fn); + auto mesherType = readMesherType(fn, override); if (mesherType == meshlib::app::staircase_mesher) { - bool compress = readStaircaseMesherCompressOption(fn); + bool compress = readStaircaseMesherCompressOption(fn, override); return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, compress}); } else if (mesherType == meshlib::app::conformal_mesher) { - return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); + return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, override)}); } else { throw std::runtime_error("Unsupported mesher type"); } @@ -167,24 +249,38 @@ int launcher(int argc, const char* argv[]) return EXIT_SUCCESS; } - // Input std::string inputFilename = vm["input"].as(); std::cout << "-- Input file is: " << inputFilename << std::endl; - Mesh mesh = readMesh(inputFilename); + std::vector objects = readObjectsFromJSON(inputFilename); + std::filesystem::path outputFolder = getFolder(inputFilename); + auto basename = getBasename(inputFilename); + Mesh firstMesh; + bool first = true; - // Mesh - auto mesher = buildMesher(mesh, inputFilename); - Mesh resultMesh = mesher->mesh(); + for (const auto& objDef : objects) { + std::cout << "\n-- Processing object: " << objDef.filename << " (group: " << objDef.group << ")" << std::endl; - std::filesystem::path outputFolder = getFolder(inputFilename); - auto basename = getBasename(inputFilename); - auto extension = readExtension(inputFilename); - - exportMeshToVTU(outputFolder / (basename + ".tessellator." + extension + ".vtk"), resultMesh); - if (readExportGridOption(inputFilename)) { - exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), resultMesh.grid); + Mesh mesh = readMesh(inputFilename, objDef); + + auto mesher = buildMesher(mesh, inputFilename, objDef.mesherOverride); + Mesh resultMesh = mesher->mesh(); + + if (first) { + firstMesh = resultMesh; + first = false; + } + + auto extension = readExtension(inputFilename, objDef.mesherOverride); + std::string outputFilename = objDef.group + ".tessellator." + extension + ".vtk"; + exportMeshToVTU(outputFolder / outputFilename, resultMesh); + std::cout << "-- Exported: " << outputFilename << std::endl; + } + + if (!first && readExportGridOption(inputFilename, std::nullopt)) { + exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), firstMesh.grid); + std::cout << "-- Exported grid: " << basename << ".tessellator.grid.vtk" << std::endl; } return EXIT_SUCCESS; diff --git a/src/app/launcher.h b/src/app/launcher.h index d65d218..bf65b34 100644 --- a/src/app/launcher.h +++ b/src/app/launcher.h @@ -1,7 +1,10 @@ #pragma once #include "types/Mesh.h" +#include "meshers/MesherBase.h" #include +#include +#include #include namespace meshlib::app { @@ -9,7 +12,16 @@ namespace meshlib::app { const std::string conformal_mesher ("conformal"); const std::string staircase_mesher ("staircase"); +struct ObjectDefinition { + std::string filename; + std::string group; + std::optional mesherOverride; +}; + int launcher(int argc, const char* argv[]); Grid parseGridFromJSON(const nlohmann::json& j); +std::vector readObjectsFromJSON(const std::string& fn); +Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef); +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const std::optional& override); } \ No newline at end of file diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index f1e6c2e..d55a561 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -162,12 +163,26 @@ vtkSmartPointer toVTKGroupsArray(const Mesh& mesh) return groupsDataArray; } +vtkSmartPointer toVTKGroupNamesArray(const Mesh& mesh) +{ + vtkNew groupNamesArray; + groupNamesArray->SetName("groupNames"); + groupNamesArray->SetNumberOfComponents(1); + + for (const auto& group : mesh.groups) { + groupNamesArray->InsertNextValue(group.name.c_str()); + } + + return groupNamesArray; +} + vtkSmartPointer elementsToVTU(const Mesh& mesh) { vtkNew vtu; vtu->SetPoints(toVTKPoints(mesh.coordinates)); vtu->GetCellData()->AddArray(toVTKGroupsArray(mesh)); + vtu->GetCellData()->AddArray(toVTKGroupNamesArray(mesh)); std::vector cellTypes; cellTypes.reserve(mesh.countElems()); diff --git a/src/types/Mesh.h b/src/types/Mesh.h index bb0d41a..dcccb37 100644 --- a/src/types/Mesh.h +++ b/src/types/Mesh.h @@ -134,10 +134,15 @@ typedef std::size_t ElementId; typedef std::vector Elements; struct Group { + std::string name; std::vector elements; + Group() = default; + Group(const std::vector& elems) : elements(elems) {} + Group(const std::string& n, const std::vector& elems) : name(n), elements(elems) {} + bool operator==(const Group& rhs) const { - return elements == rhs.elements; + return name == rhs.name && elements == rhs.elements; } std::map> buildCoordToElemMap() const { @@ -155,6 +160,7 @@ struct Group { friend class boost::serialization::access; template void serialize(Archive& ar, const unsigned int version) { + ar& name; ar& elements; } }; diff --git a/src/utils/MeshTools.cpp b/src/utils/MeshTools.cpp index b2a25f4..d271f39 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -418,4 +418,58 @@ bool isAClosedTopology(const Elements& es) return CoordGraph(es).getBoundaryGraph().getVertices().size() == 0; } +Mesh extractGroupsByName(const Mesh& mesh, const std::vector& groupNames) +{ + Mesh result; + result.grid = mesh.grid; + + std::map coordRemap; + std::map> groupCoordIds; + + for (const auto& groupName : groupNames) { + groupCoordIds[groupName].clear(); + } + + for (const auto& groupName : groupNames) { + auto it = std::find_if(mesh.groups.begin(), mesh.groups.end(), + [&groupName](const Group& g) { return g.name == groupName; }); + + if (it != mesh.groups.end()) { + result.groups.push_back(*it); + + for (const auto& elem : it->elements) { + for (const auto& vId : elem.vertices) { + std::string coordKey = groupName + "_" + std::to_string(vId); + if (coordRemap.find(coordKey) == coordRemap.end()) { + CoordinateId newVId = result.coordinates.size(); + result.coordinates.push_back(mesh.coordinates[vId]); + coordRemap[coordKey] = newVId; + groupCoordIds[groupName].push_back(vId); + } + } + } + } else { + result.groups.push_back(Group{groupName, {}}); + } + } + + for (auto& group : result.groups) { + std::string groupName = group.name; + std::map elemRemap; + + for (const auto& oldVId : groupCoordIds[groupName]) { + std::string coordKey = groupName + "_" + std::to_string(oldVId); + elemRemap[oldVId] = coordRemap[coordKey]; + } + + for (auto& elem : group.elements) { + for (auto& vId : elem.vertices) { + vId = elemRemap[vId]; + } + } + } + + return result; +} + } \ No newline at end of file diff --git a/src/utils/MeshTools.h b/src/utils/MeshTools.h index 688c0cb..5fc4546 100644 --- a/src/utils/MeshTools.h +++ b/src/utils/MeshTools.h @@ -50,4 +50,6 @@ void mergeMeshAsNewGroup(Mesh& lMesh, const Mesh& iMesh); bool isAClosedTopology(const Elements& es); +Mesh extractGroupsByName(const Mesh& mesh, const std::vector& groupNames); + } \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dd9f73f..41430ce 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -38,6 +38,7 @@ target_link_libraries(tessellator_tests if(VTK_FOUND) target_sources(tessellator_tests PRIVATE "app/vtkIOTest.cpp" + "app/launcherTest.cpp" "core/CollapserTest.cpp" "core/SlicerTest.cpp" "core/SnapperTest.cpp" diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 3942428..760a483 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -131,3 +131,78 @@ TEST_F(LauncherTest, launches_conformal_cone_case) EXPECT_EQ(exitCode, EXIT_SUCCESS); } +TEST_F(LauncherTest, readObjectsFromJSON_basic) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/basic.tessellator.json"); + EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "sphere_group"); + EXPECT_FALSE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[1].filename, "cone.stl"); + EXPECT_EQ(objects[1].group, "cone_group"); + EXPECT_FALSE(objects[1].mesherOverride.has_value()); +} + +TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); + EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_FALSE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[1].filename, "cone.stl"); + EXPECT_TRUE(objects[1].mesherOverride.has_value()); + EXPECT_EQ(objects[1].mesherOverride.value()["type"], "conformal"); +} + +TEST_F(LauncherTest, readObjectsFromJSON_singleObject) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/singleObject.tessellator.json"); + EXPECT_EQ(objects.size(), 1); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "default_group"); +} + +TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) +{ + auto objects = readObjectsFromJSON("testData/cases/sphere/sphere.tessellator.json"); + EXPECT_EQ(objects.size(), 1); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "sphere"); +} + +TEST_F(LauncherTest, launches_multiObject_basic) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/basic.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_mixedMesher) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/mixedMesher.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_singleObject) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/singleObject.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_sameFileMultipleGroups) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/sameFileMultipleGroups.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + diff --git a/test/utils/MeshToolsTest.cpp b/test/utils/MeshToolsTest.cpp index b46c30a..8078d16 100644 --- a/test/utils/MeshToolsTest.cpp +++ b/test/utils/MeshToolsTest.cpp @@ -720,4 +720,90 @@ TEST_F(MeshToolsTest, reduceGrid_epsilon_coord) } } +TEST_F(MeshToolsTest, extractGroupsByName_singleGroup) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a"}); + + EXPECT_EQ(result.groups.size(), 1); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.coordinates.size(), 3); +} + +TEST_F(MeshToolsTest, extractGroupsByName_multipleGroups) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}), + Coordinate({1.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}), + Group("group_c", {Element({2, 3}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "group_c"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.groups[1].name, "group_c"); + EXPECT_EQ(result.groups[1].elements.size(), 1); +} + +TEST_F(MeshToolsTest, extractGroupsByName_nonExistentGroup) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "non_existent"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.groups[1].name, "non_existent"); + EXPECT_EQ(result.groups[1].elements.size(), 0); +} + +TEST_F(MeshToolsTest, extractGroupsByName_coordinateRemapping) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "group_b"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].elements[0].vertices, std::vector({0, 1, 2})); + EXPECT_EQ(result.groups[1].elements[0].vertices, std::vector({3, 4})); + EXPECT_EQ(result.coordinates.size(), 5); +} + } \ No newline at end of file From eeeb05b6c5a60251a6774d0fbe6d25d1bffc7cc2 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Mon, 18 May 2026 10:25:12 +0200 Subject: [PATCH 14/15] Document multi-object support, mesher selection, and options in README --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b777cec..c920511 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ The main binary is `tessellator`, which uses a tessellator json format, which wi ``` ## JSON Format -The two main entries are as follows: +The main entries are as follows: ### `` This object must always be present and contains the structure of the grid, which will be used to slice and adjust the mesh provided. It must contain one of these two sets of entries: - ``: is an array of three positive integers which indicate the number of cells in each Cartesian direction. In case of having this entry, it also must contain a ``: - - `` is represented by an array which contairs two triplets of integers, representing the minimum and maximum values of the gread in each cartesian direction. + - `` is represented by an array which contains two triplets of integers, representing the minimum and maximum values of the grid in each cartesian direction. ```json "grid": { @@ -80,17 +80,100 @@ This object must always be present and contains the structure of the grid, which } ``` -### `` -This contains the information about the mesh file. It must contain the following entry: +### `` or `` +This contains the information about the mesh file(s). You can specify a single object or multiple objects: -- `filename`: with an string containing the name of the mesh file. Its location is relative to that of the json file. - - Example: +**Single object:** +- `filename`: A string containing the name of the mesh file. Its location is relative to that of the json file. ```json "object": {"filename": "thinCylinder.stl"} ``` +**Multiple objects:** +- `objects`: An array of object definitions. Each object can have: + - `filename`: (required) The mesh file name, relative to the JSON file location + - `group`: (optional) Group name for the object (defaults to filename without extension) + - `mesher`: (optional) Override the global mesher settings for this specific object + +```json + "objects": [ + {"filename": "object1.stl", "group": "group1"}, + {"filename": "object2.stl", "group": "group2", "mesher": {"type": "conformal"}} + ] +``` + +### `` +This optional entry configures the meshing algorithm and its options. If not specified, the staircase mesher is used with default options. + +**Mesher types:** +- `staircase` (default): Generates staircased meshes from geometric inputs +- `conformal`: Creates conformal meshes with fixed-distance grid plane intersections + +**Mesher options:** + +For **staircase** mesher: +- `compress`: (boolean, default: false) Enables surface compression to merge adjacent coplanar quads into larger surfaces + +For **conformal** mesher: +- `edgePoints`: Controls edge point snapping behavior +- `forbiddenLength`: Minimum length threshold for snapping + +**Global options:** +- `exportGrid`: (boolean, default: true) Controls whether to export the grid file + +Example with staircase mesher and compression enabled: +```json + "mesher": { + "type": "staircase", + "options": { + "compress": true, + "exportGrid": true + } + } +``` + +Example with conformal mesher: +```json + "mesher": { + "type": "conformal", + "options": { + "edgePoints": true, + "forbiddenLength": 0.001 + } + } +``` + +### Output Files +The tessellator generates output files with the following naming convention: +- `{group_name}.tessellator.str.vtk` - Staircase meshed object +- `{group_name}.tessellator.cmsh.vtk` - Conformal meshed object +- `{basename}.tessellator.grid.vtk` - Grid file (if `exportGrid` is true) + +### Complete Example +```json +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "objects": [ + {"filename": "sphere.stl"}, + {"filename": "cylinder.stl", "mesher": {"type": "conformal"}} + ], + "mesher": { + "type": "staircase", + "options": { + "compress": true, + "exportGrid": true + } + } +} +``` + ## Contributing ## Citing this work From ebf146fbcfc3a99b688a5b01b356485f77298198 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 11:20:12 +0000 Subject: [PATCH 15/15] Fix Windows JSON path conversion in launcher Agent-Logs-Url: https://github.com/OpenSEMBA/tessellator/sessions/9d4cb838-6891-4b15-a986-f5f0e798125b Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- src/app/launcher.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 7373c5c..caf6108 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -60,8 +60,8 @@ std::vector readObjectsFromJSON(const std::string& fn) if (j.contains("objects")) { for (const auto& obj : j["objects"]) { ObjectDefinition objDef; - objDef.filename = obj["filename"]; - objDef.group = obj.value("group", std::filesystem::path(obj["filename"]).stem().string()); + objDef.filename = obj["filename"].get(); + objDef.group = obj.value("group", std::filesystem::path(objDef.filename).stem().string()); if (obj.contains("mesher")) { objDef.mesherOverride = obj["mesher"]; } @@ -69,8 +69,8 @@ std::vector readObjectsFromJSON(const std::string& fn) } } else if (j.contains("object")) { ObjectDefinition objDef; - objDef.filename = j["object"]["filename"]; - objDef.group = std::filesystem::path(j["object"]["filename"]).stem().string(); + objDef.filename = j["object"]["filename"].get(); + objDef.group = std::filesystem::path(objDef.filename).stem().string(); if (j.contains("mesher")) { objDef.mesherOverride = j["mesher"]; } @@ -286,4 +286,4 @@ int launcher(int argc, const char* argv[]) return EXIT_SUCCESS; } -} \ No newline at end of file +}