Skip to content

Commit 683a392

Browse files
committed
More tests and splitter
1 parent b2c211a commit 683a392

4 files changed

Lines changed: 334 additions & 2 deletions

File tree

src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ add_library(tessellator-core
77
"Snapper.cpp"
88
"Smoother.cpp"
99
"SmootherTools.cpp"
10+
"Splitter.cpp"
1011
"Staircaser.cpp"
1112
)
1213

src/core/Splitter.cpp

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
#include "Splitter.h"
2+
3+
#include "utils/GridTools.h"
4+
5+
namespace meshlib::core {
6+
7+
std::size_t Splitter::splitSurfaces(Mesh& mesh) {
8+
std::size_t totalNewQuads = 0;
9+
10+
for (GroupId g = 0; g < mesh.groups.size(); g++) {
11+
std::vector<Element> newElements;
12+
13+
for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) {
14+
const Element& elem = mesh.groups[g].elements[e];
15+
if (elem.type == Element::Type::Surface) {
16+
// Split this surface into unit quads
17+
std::map<Coordinate, CoordinateId> coordMap;
18+
19+
// Build initial coord map from existing coordinates
20+
for (CoordinateId i = 0; i < static_cast<CoordinateId>(mesh.coordinates.size()); ++i) {
21+
coordMap[mesh.coordinates[i]] = i;
22+
}
23+
24+
std::vector<Element> splitQuads = splitSurface_(
25+
elem, mesh.coordinates, mesh.grid, coordMap);
26+
27+
// Add new coordinates from coordMap
28+
for (const auto& [coord, id] : coordMap) {
29+
if (id >= mesh.coordinates.size()) {
30+
mesh.coordinates.push_back(coord);
31+
}
32+
}
33+
34+
newElements.insert(newElements.end(), splitQuads.begin(), splitQuads.end());
35+
totalNewQuads += splitQuads.size();
36+
} else {
37+
newElements.push_back(elem);
38+
}
39+
}
40+
41+
mesh.groups[g].elements = std::move(newElements);
42+
}
43+
44+
return totalNewQuads;
45+
}
46+
47+
std::vector<Element> Splitter::splitSurface_(
48+
const Element& surface,
49+
const std::vector<Coordinate>& coords,
50+
const Grid& grid,
51+
std::map<Coordinate, CoordinateId>& coordMap) {
52+
std::vector<Element> quads;
53+
54+
// Get the plane orientation of the surface
55+
auto [normalAxis, plane] = getSurfacePlane_(surface, coords);
56+
57+
// Get the grid cell bounds
58+
auto [minCell, maxCell] = getSurfaceBounds_(surface, coords);
59+
60+
// Determine the 2D axes in the plane
61+
Axis axis1 = (normalAxis + 1) % 3;
62+
Axis axis2 = (normalAxis + 2) % 3;
63+
64+
// Generate unit quads for each cell in the bounds
65+
for (CellDir i = minCell(axis1); i < maxCell(axis1); i++) {
66+
for (CellDir j = minCell(axis2); j < maxCell(axis2); j++) {
67+
Element quad = createUnitQuad_(
68+
plane, normalAxis, i, j, grid, coordMap);
69+
quads.push_back(quad);
70+
}
71+
}
72+
73+
return quads;
74+
}
75+
76+
std::pair<Cell, Cell> Splitter::getSurfaceBounds_(
77+
const Element& surface,
78+
const std::vector<Coordinate>& coords) {
79+
Cell minCell = {0, 0, 0};
80+
Cell maxCell = {0, 0, 0};
81+
82+
bool first = true;
83+
for (CoordinateId vid : surface.vertices) {
84+
Cell cell = utils::GridTools::toCell(coords[vid]);
85+
if (first) {
86+
minCell = cell;
87+
maxCell = cell;
88+
first = false;
89+
} else {
90+
for (Axis d = 0; d < 3; d++) {
91+
minCell(d) = std::min(minCell(d), cell(d));
92+
maxCell(d) = std::max(maxCell(d), cell(d));
93+
}
94+
}
95+
}
96+
97+
// maxCell represents the cell containing the max coordinate value
98+
// For a surface spanning cells 0 to N-1, the max coordinate is at grid[N]
99+
// So maxCell should be set to N (the cell index of the max coord), which is already correct
100+
// The loop below will iterate i < maxCell, giving cells 0 to maxCell-1
101+
// No increment needed
102+
103+
return {minCell, maxCell};
104+
}
105+
106+
std::pair<Axis, CellDir> Splitter::getSurfacePlane_(
107+
const Element& surface,
108+
const std::vector<Coordinate>& coords) {
109+
// Find which axis has constant coordinate (the normal axis)
110+
for (Axis d = 0; d < 3; d++) {
111+
Cell cell0 = utils::GridTools::toCell(coords[surface.vertices[0]]);
112+
bool allSame = true;
113+
for (CoordinateId vid : surface.vertices) {
114+
Cell cell = utils::GridTools::toCell(coords[vid]);
115+
if (cell(d) != cell0(d)) {
116+
allSame = false;
117+
break;
118+
}
119+
}
120+
if (allSame) {
121+
return {d, cell0(d)};
122+
}
123+
}
124+
125+
// Fallback (should not happen for valid surfaces)
126+
return {0, 0};
127+
}
128+
129+
Element Splitter::createUnitQuad_(
130+
CellDir plane,
131+
Axis normalAxis,
132+
CellDir xCell,
133+
CellDir yCell,
134+
const Grid& grid,
135+
std::map<Coordinate, CoordinateId>& coordMap) {
136+
Axis axis1 = (normalAxis + 1) % 3;
137+
Axis axis2 = (normalAxis + 2) % 3;
138+
139+
// Create 4 corner coordinates for the unit quad
140+
std::array<Coordinate, 4> corners;
141+
corners[0] = Coordinate({0, 0, 0});
142+
corners[1] = Coordinate({0, 0, 0});
143+
corners[2] = Coordinate({0, 0, 0});
144+
corners[3] = Coordinate({0, 0, 0});
145+
146+
// Set coordinates for each corner
147+
corners[0](normalAxis) = grid[normalAxis][plane];
148+
corners[0](axis1) = grid[axis1][xCell];
149+
corners[0](axis2) = grid[axis2][yCell];
150+
151+
corners[1](normalAxis) = grid[normalAxis][plane];
152+
corners[1](axis1) = grid[axis1][xCell + 1];
153+
corners[1](axis2) = grid[axis2][yCell];
154+
155+
corners[2](normalAxis) = grid[normalAxis][plane];
156+
corners[2](axis1) = grid[axis1][xCell + 1];
157+
corners[2](axis2) = grid[axis2][yCell + 1];
158+
159+
corners[3](normalAxis) = grid[normalAxis][plane];
160+
corners[3](axis1) = grid[axis1][xCell];
161+
corners[3](axis2) = grid[axis2][yCell + 1];
162+
163+
// Get or create coordinate IDs
164+
std::array<CoordinateId, 4> vids;
165+
for (int i = 0; i < 4; i++) {
166+
auto it = coordMap.find(corners[i]);
167+
if (it != coordMap.end()) {
168+
vids[i] = it->second;
169+
} else {
170+
coordMap[corners[i]] = static_cast<CoordinateId>(coordMap.size());
171+
vids[i] = coordMap[corners[i]];
172+
}
173+
}
174+
175+
Element quad;
176+
quad.type = Element::Type::Surface;
177+
quad.vertices = {vids[0], vids[1], vids[2], vids[3]};
178+
179+
return quad;
180+
}
181+
182+
}

src/core/Splitter.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#pragma once
2+
3+
#include "types/Mesh.h"
4+
#include "utils/Types.h"
5+
6+
namespace meshlib::core {
7+
8+
class Splitter {
9+
public:
10+
// Split all surfaces in mesh into unit quads (1x1 grid cells)
11+
// Returns number of new quads created
12+
static std::size_t splitSurfaces(Mesh& mesh);
13+
14+
private:
15+
// Split a single surface into unit quads
16+
static std::vector<Element> splitSurface_(
17+
const Element& surface,
18+
const std::vector<Coordinate>& coords,
19+
const Grid& grid,
20+
std::map<Coordinate, CoordinateId>& coordMap);
21+
22+
// Get grid cell bounds for a surface
23+
static std::pair<Cell, Cell> getSurfaceBounds_(
24+
const Element& surface,
25+
const std::vector<Coordinate>& coords);
26+
27+
// Determine the plane orientation of a surface (which axis is normal)
28+
static std::pair<Axis, CellDir> getSurfacePlane_(
29+
const Element& surface,
30+
const std::vector<Coordinate>& coords);
31+
32+
// Create a unit quad at given grid cell position
33+
static Element createUnitQuad_(
34+
CellDir plane,
35+
Axis normalAxis,
36+
CellDir xCell,
37+
CellDir yCell,
38+
const Grid& grid,
39+
std::map<Coordinate, CoordinateId>& coordMap);
40+
};
41+
42+
}

test/core/CompressorTest.cpp

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <gtest/gtest.h>
22
#include "core/Compressor.h"
3+
#include "core/Splitter.h"
34
#include "MeshFixtures.h"
45
#include "utils/MeshTools.h"
56

@@ -92,12 +93,18 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) {
9293

9394
TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) {
9495
// Create 8 quads forming a ring with a hole in the middle
95-
// Should be merged into one surface with inner contour
96+
// The ring decomposes into 3 rectangles (left col, right col, center cols)
9697

9798
Mesh mesh;
9899
mesh.grid = grid_;
99100

100101
// Outer ring of quads (leaving center 1,1 to 2,2 empty)
102+
// Layout:
103+
// x=0 x=1 x=2 x=3
104+
// y=3 [5] [8] [6]
105+
// y=2 [3] hole [4]
106+
// y=1 [1] [7] [2]
107+
// y=0
101108
// Bottom row
102109
addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0});
103110
addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0});
@@ -115,7 +122,14 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) {
115122

116123
auto merged = core::Compressor::compressSurfaces(mesh);
117124

118-
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u);
125+
auto finalCount = countMeshElementsIf(mesh, isQuad);
126+
127+
// Optimal decomposition: 3 rectangles
128+
// - Left column (quads 1,3,5): cells x=0, y=0-3
129+
// - Right column (quads 2,4,6): cells x=2-3, y=0-3
130+
// - Center (quads 7,8): cells x=1-2, y=0 and y=2-3
131+
EXPECT_EQ(finalCount, 3u);
132+
EXPECT_EQ(merged, 5u); // 8 - 3 = 5 surfaces merged
119133
}
120134

121135
TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) {
@@ -137,4 +151,97 @@ TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) {
137151
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u);
138152
}
139153

154+
TEST_F(CompressorTest, CompressAndSplit2x2GridRoundTrip) {
155+
// Create 4 quads in 2x2 grid, compress to 1 surface, split back to 4 quads
156+
157+
Mesh mesh;
158+
mesh.grid = grid_;
159+
160+
// 2x2 grid of quads
161+
addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0});
162+
addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0});
163+
addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0});
164+
addQuad(mesh, {1, 1, 0}, {2, 1, 0}, {2, 2, 0}, {1, 2, 0});
165+
166+
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u);
167+
168+
// Compress: 4 quads -> 1 surface
169+
auto merged = core::Compressor::compressSurfaces(mesh);
170+
EXPECT_EQ(merged, 3u);
171+
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u);
172+
173+
// Debug: print compressed surface
174+
std::cerr << "Compressed surface vertices: ";
175+
for (auto vid : mesh.groups[0].elements[0].vertices) {
176+
std::cerr << "(" << mesh.coordinates[vid](0) << ","
177+
<< mesh.coordinates[vid](1) << ","
178+
<< mesh.coordinates[vid](2) << ") ";
179+
}
180+
std::cerr << std::endl;
181+
182+
// Split: 1 surface -> 4 quads
183+
auto splitCount = core::Splitter::splitSurfaces(mesh);
184+
std::cout << "Split count: " << splitCount << std::endl;
185+
EXPECT_EQ(splitCount, 4u);
186+
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u);
187+
}
188+
189+
TEST_F(CompressorTest, CompressAndSplitRingRoundTrip) {
190+
// Create ring of 8 quads, compress, split back
191+
192+
Mesh mesh;
193+
mesh.grid = grid_;
194+
195+
// Ring of 8 quads (same as CompressWithHoleCreatesInnerContour)
196+
addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0});
197+
addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0});
198+
addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0});
199+
addQuad(mesh, {2, 1, 0}, {3, 1, 0}, {3, 2, 0}, {2, 2, 0});
200+
addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0});
201+
addQuad(mesh, {2, 2, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0});
202+
addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0});
203+
addQuad(mesh, {1, 3, 0}, {2, 3, 0}, {2, 2, 0}, {1, 2, 0});
204+
205+
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u);
206+
207+
// Compress: 8 quads -> 3 surfaces
208+
auto merged = core::Compressor::compressSurfaces(mesh);
209+
EXPECT_EQ(merged, 5u);
210+
auto compressedCount = countMeshElementsIf(mesh, isQuad);
211+
EXPECT_EQ(compressedCount, 3u);
212+
213+
// Split: 3 surfaces -> 8 quads
214+
auto splitCount = core::Splitter::splitSurfaces(mesh);
215+
EXPECT_EQ(splitCount, 8u);
216+
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u);
217+
}
218+
219+
TEST_F(CompressorTest, CompressAndSplit3x3GridRoundTrip) {
220+
// Create 9 quads in 3x3 grid, compress to 1 surface, split back to 9 quads
221+
222+
Mesh mesh;
223+
mesh.grid = grid_;
224+
225+
// 3x3 grid of quads
226+
for (int i = 0; i < 3; i++) {
227+
for (int j = 0; j < 3; j++) {
228+
addQuad(mesh,
229+
{i, j, 0}, {i+1, j, 0},
230+
{i+1, j+1, 0}, {i, j+1, 0});
231+
}
232+
}
233+
234+
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u);
235+
236+
// Compress: 9 quads -> 1 surface
237+
auto merged = core::Compressor::compressSurfaces(mesh);
238+
EXPECT_EQ(merged, 8u);
239+
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u);
240+
241+
// Split: 1 surface -> 9 quads
242+
auto splitCount = core::Splitter::splitSurfaces(mesh);
243+
EXPECT_EQ(splitCount, 9u);
244+
EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u);
245+
}
246+
140247
}

0 commit comments

Comments
 (0)