Skip to content

Commit 11772a3

Browse files
authored
Add option to deduplicate edges in edge triangulation (#64)
* start deduplicate edges * add option to remove duplicate edges from path triangulation * fix tests * fix on linux * disable debug mode * improve documentation * fix traingle numbers
1 parent 489f6d4 commit 11772a3

3 files changed

Lines changed: 146 additions & 12 deletions

File tree

src/PartSegCore_compiled_backend/triangulate.pyx

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ from libcpp cimport bool
1313
from libcpp.unordered_set cimport unordered_set
1414
from libcpp.utility cimport pair
1515
from libcpp.vector cimport vector
16-
from libcpp.unordered_map cimport unordered_map
17-
from libcpp.algorithm cimport sort
1816

1917

2018
cdef extern from "triangulation/point.hpp" namespace "partsegcore::point":
@@ -96,6 +94,7 @@ cdef extern from "triangulation/triangulate.hpp" namespace "partsegcore::triangu
9694
pair[vector[Triangle], vector[Point]] triangulate_polygon_face(const vector[Point]& polygon) except + nogil
9795
pair[vector[Triangle], vector[Point]] triangulate_polygon_face(const vector[vector[Point]]& polygon_list) except + nogil
9896
PathTriangulation triangulate_path_edge(const vector[Point]& path, bool closed, float limit, bool bevel) except + nogil
97+
vector[vector[Point]] split_polygon_on_repeated_edges(const vector[Point]& polygon) except + nogil
9998

10099

101100
ctypedef fused float_types:
@@ -429,14 +428,15 @@ def triangulate_path_edge_numpy(cnp.ndarray[cnp.float32_t, ndim=2] path, bool cl
429428
)
430429

431430

432-
def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray]) -> tuple[tuple[np.ndarray, np.ndarray], tuple[np.ndarray, np.ndarray, np.ndarray]]:
431+
def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray], split_edges: bool=False) -> tuple[tuple[np.ndarray, np.ndarray], tuple[np.ndarray, np.ndarray, np.ndarray]]:
433432
""" Triangulate polygon"""
434433
cdef vector[Point] polygon_vector
435-
cdef vector[vector[Point]] polygon_vector_list
434+
cdef vector[vector[Point]] polygon_vector_list, edge_split_list
436435
cdef Point p1, p2
437436
cdef pair[vector[Triangle], vector[Point]] triangulation_result
438437
cdef vector[PathTriangulation] edge_result
439438
cdef cnp.ndarray[cnp.uint32_t, ndim=2] triangles, edge_triangles
439+
cdef size_t triangle_count = 0
440440

441441
cdef cnp.ndarray[cnp.float32_t, ndim=2] points, edge_offsets, edges_centers, polygon
442442
cdef size_t i, j, len_path, edge_triangle_count, edge_center_count, edge_triangle_index, edge_center_index
@@ -457,8 +457,14 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray]) -> tupl
457457
if polygon_vector.size() > 1 and polygon_vector.front() == polygon_vector.back():
458458
polygon_vector.pop_back()
459459
polygon_vector_list.push_back(polygon_vector)
460-
with cython.nogil:
461-
edge_result.push_back(triangulate_path_edge(polygon_vector, True, 3.0, False))
460+
if split_edges:
461+
with cython.nogil:
462+
edge_split_list = split_polygon_on_repeated_edges(polygon_vector)
463+
for edge_li in edge_split_list:
464+
edge_result.push_back(triangulate_path_edge(edge_li, True, 3.0, False))
465+
else:
466+
with cython.nogil:
467+
edge_result.push_back(triangulate_path_edge(polygon_vector, True, 3.0, False))
462468

463469
with cython.nogil:
464470
triangulation_result = triangulate_polygon_face(polygon_vector_list)
@@ -470,7 +476,7 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray]) -> tupl
470476
triangles[i, 1] = triangulation_result.first[i].y
471477
triangles[i, 2] = triangulation_result.first[i].z
472478

473-
points =np.empty((triangulation_result.second.size(), 2), dtype=np.float32)
479+
points = np.empty((triangulation_result.second.size(), 2), dtype=np.float32)
474480
for i in range(triangulation_result.second.size()):
475481
points[i, 0] = triangulation_result.second[i].x
476482
points[i, 1] = triangulation_result.second[i].y
@@ -489,10 +495,11 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray]) -> tupl
489495
edge_center_index = 0
490496
for i in range(edge_result.size()):
491497
for j in range(edge_result[i].triangles.size()):
492-
edge_triangles[edge_triangle_index, 0] = edge_result[i].triangles[j].x
493-
edge_triangles[edge_triangle_index, 1] = edge_result[i].triangles[j].y
494-
edge_triangles[edge_triangle_index, 2] = edge_result[i].triangles[j].z
498+
edge_triangles[edge_triangle_index, 0] = edge_result[i].triangles[j].x + triangle_count
499+
edge_triangles[edge_triangle_index, 1] = edge_result[i].triangles[j].y + triangle_count
500+
edge_triangles[edge_triangle_index, 2] = edge_result[i].triangles[j].z + triangle_count
495501
edge_triangle_index += 1
502+
triangle_count += edge_result[i].centers.size()
496503

497504
for j in range(edge_result[i].centers.size()):
498505
edges_centers[edge_center_index, 0] = edge_result[i].centers[j].x
@@ -511,3 +518,20 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray]) -> tupl
511518
edge_triangles,
512519
)
513520
)
521+
522+
523+
def split_polygon_on_repeated_edges_py(polygon: Sequence[Sequence[float]]) -> list[list[tuple[float, float]]]:
524+
""" Split polygon on repeated edges"""
525+
cdef vector[Point] polygon_vector
526+
cdef vector[vector[Point]] result
527+
cdef Point p1, p2
528+
529+
polygon_vector.reserve(len(polygon))
530+
for point in polygon:
531+
polygon_vector.push_back(Point(point[0], point[1]))
532+
533+
result = split_polygon_on_repeated_edges(polygon_vector)
534+
return [
535+
[(point.x, point.y) for point in polygon_vector]
536+
for polygon_vector in result
537+
]

src/PartSegCore_compiled_backend/triangulation/triangulate.hpp

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
#define PARTSEGCORE_TRIANGULATE_H
33

44
#include <algorithm>
5-
#include <cmath>
65
#include <map>
7-
#include <memory>
6+
#include <memory> // memory header is required on linux, and not on macos
87
#include <set>
98
#include <sstream>
109
#include <unordered_map>
@@ -1288,6 +1287,87 @@ inline PathTriangulation triangulate_path_edge(
12881287
return result;
12891288
}
12901289

1290+
/**
1291+
* Represents an edge in a graph structure used for polygon processing.
1292+
* Each edge contains a reference to its opposite point and a flag to track
1293+
* if it has been visited during graph traversal.
1294+
*/
1295+
struct GraphEdge {
1296+
point::Point opposite_point;
1297+
bool visited;
1298+
explicit GraphEdge(point::Point p) : opposite_point(p), visited(false) {}
1299+
};
1300+
1301+
/**
1302+
* Represents a node in a graph structure used for polygon processing.
1303+
* Each node contains its edges, a sub-index for traversal tracking,
1304+
* and a visited flag for graph traversal.
1305+
*/
1306+
struct GraphNode {
1307+
std::vector<GraphEdge> edges;
1308+
std::size_t sub_index;
1309+
bool visited;
1310+
1311+
GraphNode() : sub_index(0), visited(false) {}
1312+
};
1313+
1314+
/**
1315+
* Splits a polygon into sub-polygons by identifying and removing edges that
1316+
* appear more than once in the polygon's edge list.
1317+
*
1318+
* This function processes the given polygon and separates it wherever an
1319+
* edge is repeated. It generates a collection of sub-polygons such that each
1320+
* resulting sub-polygon contains unique edges. This operation can help to
1321+
* resolve ambiguities in complex or self-intersecting polygons.
1322+
*
1323+
* @param polygon The input polygon represented as a list of edges.
1324+
*
1325+
* @return A vector of sub-polygons, where each sub-polygon is free of repeated
1326+
* edges.
1327+
*/
1328+
inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
1329+
const std::vector<point::Point> &polygon) {
1330+
auto edges_dedup = calc_dedup_edges({polygon});
1331+
std::vector<std::vector<point::Point>> result;
1332+
point::Segment segment;
1333+
1334+
std::unordered_set edges_set(edges_dedup.begin(), edges_dedup.end());
1335+
std::unordered_map<point::Point, GraphNode> edges_map;
1336+
for (std::size_t i = 0; i < polygon.size() - 1; i++) {
1337+
segment = {polygon[i], polygon[(i + 1)]};
1338+
if (edges_set.count(segment) > 0) {
1339+
edges_map[polygon[i]].edges.emplace_back(polygon[i + 1]);
1340+
}
1341+
}
1342+
segment = {polygon.back(), polygon.front()};
1343+
if (edges_set.count(segment) > 0) {
1344+
edges_map[polygon.back()].edges.emplace_back(polygon.front());
1345+
}
1346+
for (auto &edge : edges_map) {
1347+
if (edge.second.visited) {
1348+
continue;
1349+
}
1350+
edge.second.visited = true;
1351+
std::vector<point::Point> new_polygon;
1352+
new_polygon.push_back(edge.first);
1353+
auto *current_edge = &edge.second;
1354+
while (current_edge->sub_index < current_edge->edges.size()) {
1355+
auto *prev = current_edge;
1356+
auto next_point =
1357+
current_edge->edges[current_edge->sub_index].opposite_point;
1358+
current_edge = &edges_map.at(next_point);
1359+
prev->sub_index++;
1360+
current_edge->visited = true;
1361+
new_polygon.push_back(next_point);
1362+
}
1363+
while (new_polygon.front() == new_polygon.back()) {
1364+
new_polygon.pop_back();
1365+
}
1366+
result.push_back(new_polygon);
1367+
}
1368+
return result;
1369+
}
1370+
12911371
} // namespace partsegcore::triangulation
12921372

12931373
#endif // PARTSEGCORE_TRIANGULATE_H

src/tests/test_triangulate.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
on_segment,
1111
orientation,
1212
segment_left_to_right_comparator,
13+
split_polygon_on_repeated_edges_py,
1314
triangle_convex_polygon,
1415
triangulate_monotone_polygon_py,
1516
triangulate_path_edge_numpy,
@@ -661,3 +662,32 @@ def test_triangulate_polygon_with_edge_numpy_li(polygon, expected):
661662
triangles_ = _renumerate_triangles(polygon, points, triangles)
662663
assert triangles_ == expected
663664
assert centers.shape == offsets.shape
665+
666+
667+
def test_split_polygon_on_repeated_edges_py_no_split():
668+
res = split_polygon_on_repeated_edges_py([[0, 0], [0, 1], [1, 1], [1, 0]])
669+
assert len(res) == 1
670+
assert len(res[0]) == 4
671+
idx = res[0].index((0, 0))
672+
assert res[0][idx:] + res[0][:idx] == [(0, 0), (0, 1), (1, 1), (1, 0)]
673+
674+
675+
def test_split_polygon_on_repeated_edges_py_square_in_square():
676+
res = split_polygon_on_repeated_edges_py(
677+
[[0, 0], [0, 5], [1, 5], [1, 1], [9, 1], [9, 9], [1, 9], [1, 5], [0, 5], [0, 10], [10, 10], [10, 0]]
678+
)
679+
assert len(res) == 2
680+
assert len(res[0]) == 5
681+
assert len(res[1]) == 5
682+
# idx = res[0].index((0, 0))
683+
# assert res[0][idx:] + res[0][:idx] == [(0, 0), (0, 1), (1, 1), (1, 0)]
684+
685+
686+
@pytest.mark.parametrize(('split_edges', 'triangles'), [(True, 20), (False, 24)])
687+
def test_splitting_edges(split_edges, triangles):
688+
polygon = np.array(
689+
[[0, 0], [0, 5], [1, 5], [1, 1], [9, 1], [9, 9], [1, 9], [1, 5], [0, 5], [0, 10], [10, 10], [10, 0]],
690+
dtype=np.float32,
691+
)
692+
triangles_ = triangulate_polygon_with_edge_numpy_li([polygon], split_edges=split_edges)[1][2]
693+
assert len(triangles_) == triangles

0 commit comments

Comments
 (0)