From 8c875d9e55647491ac4d19690a2fedcc821b01d1 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Fri, 24 Jan 2025 17:53:05 +0100 Subject: [PATCH 1/7] start deduplicate edges --- .../triangulate.pyx | 2 -- .../triangulation/triangulate.hpp | 27 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/PartSegCore_compiled_backend/triangulate.pyx b/src/PartSegCore_compiled_backend/triangulate.pyx index 45e6640..32b4ffc 100644 --- a/src/PartSegCore_compiled_backend/triangulate.pyx +++ b/src/PartSegCore_compiled_backend/triangulate.pyx @@ -13,8 +13,6 @@ from libcpp cimport bool from libcpp.unordered_set cimport unordered_set from libcpp.utility cimport pair from libcpp.vector cimport vector -from libcpp.unordered_map cimport unordered_map -from libcpp.algorithm cimport sort cdef extern from "triangulation/point.hpp" namespace "partsegcore::point": diff --git a/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp b/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp index 09bdb81..c943934 100644 --- a/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp +++ b/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -1288,6 +1289,32 @@ inline PathTriangulation triangulate_path_edge( return result; } +std::vector> split_polygon_on_repeated_edges( + std::vector polygon) { + std::unordered_map> point_to_index; + std::stack stack = {0}; + for (std::size_t i = 0; i < polygon.size(); i++) { + point_to_index[polygon[i]].emplace_back(i); + } + if (point_to_index.size() == polygon.size()) { + // no repeated points + return {polygon}; + } + std::vector> new_polygons_list; + new_polygons_list.push_back({}); + for (std::size_t i = 0; i < polygon.size(); i++) { + auto &point = polygon[i]; + if (point_to_index[point].size() == 1) { + new_polygons_list[stack.top()].push_back(point); + } else { + new_polygons_list[stack.top()].push_back(point); + if stack + .push(new_polygons_list.size()); + new_polygons_list.push_back({}); + } + } +} + } // namespace partsegcore::triangulation #endif // PARTSEGCORE_TRIANGULATE_H From 65c3636a433b021af45dd09703266c604dc2dd1c Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Tue, 28 Jan 2025 16:35:21 +0100 Subject: [PATCH 2/7] add option to remove duplicate edges from path triangulation --- pyproject.toml | 2 +- .../triangulate.pyx | 32 +++++++- .../triangulation/triangulate.hpp | 76 +++++++++++++------ src/tests/test_triangulate.py | 28 +++++++ 4 files changed, 110 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5918c8c..5a758a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ sdist.include = ["src/PartSegCore_compiled_backend/version.py"] cmake.version = ">=3.21" sdist.exclude = [".github", "tox.ini", "build_utils", "notebooks", ".readthedocs.yaml"] wheel.exclude = ["**.pyx"] -#cmake.build-type = "Debug" +cmake.build-type = "Debug" [project] diff --git a/src/PartSegCore_compiled_backend/triangulate.pyx b/src/PartSegCore_compiled_backend/triangulate.pyx index 32b4ffc..db71655 100644 --- a/src/PartSegCore_compiled_backend/triangulate.pyx +++ b/src/PartSegCore_compiled_backend/triangulate.pyx @@ -94,6 +94,7 @@ cdef extern from "triangulation/triangulate.hpp" namespace "partsegcore::triangu pair[vector[Triangle], vector[Point]] triangulate_polygon_face(const vector[Point]& polygon) except + nogil pair[vector[Triangle], vector[Point]] triangulate_polygon_face(const vector[vector[Point]]& polygon_list) except + nogil PathTriangulation triangulate_path_edge(const vector[Point]& path, bool closed, float limit, bool bevel) except + nogil + vector[vector[Point]] split_polygon_on_repeated_edges(const vector[Point]& polygon) except + nogil ctypedef fused float_types: @@ -427,10 +428,10 @@ def triangulate_path_edge_numpy(cnp.ndarray[cnp.float32_t, ndim=2] path, bool cl ) -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]]: +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]]: """ Triangulate polygon""" cdef vector[Point] polygon_vector - cdef vector[vector[Point]] polygon_vector_list + cdef vector[vector[Point]] polygon_vector_list, edge_split_list cdef Point p1, p2 cdef pair[vector[Triangle], vector[Point]] triangulation_result cdef vector[PathTriangulation] edge_result @@ -455,8 +456,14 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray]) -> tupl if polygon_vector.size() > 1 and polygon_vector.front() == polygon_vector.back(): polygon_vector.pop_back() polygon_vector_list.push_back(polygon_vector) - with cython.nogil: - edge_result.push_back(triangulate_path_edge(polygon_vector, True, 3.0, False)) + if split_edges: + with cython.nogil: + edge_split_list = split_polygon_on_repeated_edges(polygon_vector) + for edge_li in edge_split_list: + edge_result.push_back(triangulate_path_edge(edge_li, True, 3.0, False)) + else: + with cython.nogil: + edge_result.push_back(triangulate_path_edge(polygon_vector, True, 3.0, False)) with cython.nogil: triangulation_result = triangulate_polygon_face(polygon_vector_list) @@ -509,3 +516,20 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray]) -> tupl edge_triangles, ) ) + + +def split_polygon_on_repeated_edges_py(polygon: Sequence[Sequence[float]]) -> list[list[tuple[float, float]]]: + """ Split polygon on repeated edges""" + cdef vector[Point] polygon_vector + cdef vector[vector[Point]] result + cdef Point p1, p2 + + polygon_vector.reserve(len(polygon)) + for point in polygon: + polygon_vector.push_back(Point(point[0], point[1])) + + result = split_polygon_on_repeated_edges(polygon_vector) + return [ + [(point.x, point.y) for point in polygon_vector] + for polygon_vector in result + ] diff --git a/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp b/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp index c943934..b0a2085 100644 --- a/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp +++ b/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp @@ -2,12 +2,10 @@ #define PARTSEGCORE_TRIANGULATE_H #include -#include #include -#include +#include #include #include -#include #include #include #include @@ -1289,30 +1287,62 @@ inline PathTriangulation triangulate_path_edge( return result; } -std::vector> split_polygon_on_repeated_edges( - std::vector polygon) { - std::unordered_map> point_to_index; - std::stack stack = {0}; - for (std::size_t i = 0; i < polygon.size(); i++) { - point_to_index[polygon[i]].emplace_back(i); +struct GraphEdge { + point::Point opposite_point; + bool visited; + explicit GraphEdge(point::Point p) : opposite_point(p), visited(false) {} +}; + +struct GraphNode { + std::vector edges; + std::size_t sub_index; + bool visited; + + GraphNode() : sub_index(0), visited(false) {} +}; + +inline std::vector> split_polygon_on_repeated_edges( + const std::vector &polygon) { + auto edges_dedup = calc_dedup_edges({polygon}); + std::vector visited(polygon.size(), false); + std::vector> result; + point::Segment segment; + + std::unordered_set edges_set(edges_dedup.begin(), edges_dedup.end()); + std::unordered_map edges_map; + for (std::size_t i = 0; i < polygon.size() - 1; i++) { + segment = {polygon[i], polygon[(i + 1)]}; + if (edges_set.count(segment) > 0) { + edges_map[polygon[i]].edges.emplace_back(polygon[i + 1]); + } } - if (point_to_index.size() == polygon.size()) { - // no repeated points - return {polygon}; + segment = {polygon.back(), polygon.front()}; + if (edges_set.count(segment) > 0) { + edges_map[polygon.back()].edges.emplace_back(polygon.front()); } - std::vector> new_polygons_list; - new_polygons_list.push_back({}); - for (std::size_t i = 0; i < polygon.size(); i++) { - auto &point = polygon[i]; - if (point_to_index[point].size() == 1) { - new_polygons_list[stack.top()].push_back(point); - } else { - new_polygons_list[stack.top()].push_back(point); - if stack - .push(new_polygons_list.size()); - new_polygons_list.push_back({}); + for (auto &edge : edges_map) { + if (edge.second.visited) { + continue; + } + edge.second.visited = true; + std::vector new_polygon; + new_polygon.push_back(edge.first); + auto *current_edge = &edge.second; + while (current_edge->sub_index < current_edge->edges.size()) { + auto *prev = current_edge; + auto next_point = + current_edge->edges[current_edge->sub_index].opposite_point; + current_edge = &edges_map.at(next_point); + prev->sub_index++; + current_edge->visited = true; + new_polygon.push_back(next_point); + } + while (new_polygon.front() == new_polygon.back()) { + new_polygon.pop_back(); } + result.push_back(new_polygon); } + return result; } } // namespace partsegcore::triangulation diff --git a/src/tests/test_triangulate.py b/src/tests/test_triangulate.py index f436ecb..844389e 100644 --- a/src/tests/test_triangulate.py +++ b/src/tests/test_triangulate.py @@ -10,6 +10,7 @@ on_segment, orientation, segment_left_to_right_comparator, + split_polygon_on_repeated_edges_py, triangle_convex_polygon, triangulate_monotone_polygon_py, triangulate_path_edge_numpy, @@ -661,3 +662,30 @@ def test_triangulate_polygon_with_edge_numpy_li(polygon, expected): triangles_ = _renumerate_triangles(polygon, points, triangles) assert triangles_ == expected assert centers.shape == offsets.shape + + +def test_split_polygon_on_repeated_edges_py_no_split(): + res = split_polygon_on_repeated_edges_py([[0, 0], [0, 1], [1, 1], [1, 0]]) + assert len(res) == 1 + assert len(res[0]) == 4 + idx = res[0].index((0, 0)) + assert res[0][idx:] + res[0][:idx] == [(0, 0), (0, 1), (1, 1), (1, 0)] + + +def test_split_polygon_on_repeated_edges_py_square_in_square(): + res = split_polygon_on_repeated_edges_py() + assert len(res) == 2 + assert len(res[0]) == 5 + assert len(res[1]) == 5 + # idx = res[0].index((0, 0)) + # assert res[0][idx:] + res[0][:idx] == [(0, 0), (0, 1), (1, 1), (1, 0)] + + +@pytest.mark.parametrize(('split_edges', 'triangles'), [(True, 20), (False, 24)]) +def test_splitting_edges(split_edges, triangles): + polygon = np.array( + [[0, 0], [0, 5], [1, 5], [1, 1], [9, 1], [9, 9], [1, 9], [1, 5], [0, 5], [0, 10], [10, 10], [10, 0]], + dtype=np.float32, + ) + triangles_ = triangulate_polygon_with_edge_numpy_li([polygon], split_edges=split_edges)[1][2] + assert len(triangles_) == triangles From 17eb20a11e7ff176ea0b10e42efdd24cf67980eb Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Tue, 28 Jan 2025 16:37:54 +0100 Subject: [PATCH 3/7] fix tests --- src/tests/test_triangulate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/test_triangulate.py b/src/tests/test_triangulate.py index 844389e..f5b7286 100644 --- a/src/tests/test_triangulate.py +++ b/src/tests/test_triangulate.py @@ -673,7 +673,9 @@ def test_split_polygon_on_repeated_edges_py_no_split(): def test_split_polygon_on_repeated_edges_py_square_in_square(): - res = split_polygon_on_repeated_edges_py() + res = split_polygon_on_repeated_edges_py( + [[0, 0], [0, 5], [1, 5], [1, 1], [9, 1], [9, 9], [1, 9], [1, 5], [0, 5], [0, 10], [10, 10], [10, 0]] + ) assert len(res) == 2 assert len(res[0]) == 5 assert len(res[1]) == 5 From 10512a09b9c4dbe1ab6a5819873008dbe8f25c7e Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Tue, 28 Jan 2025 20:46:16 +0100 Subject: [PATCH 4/7] fix on linux --- src/PartSegCore_compiled_backend/triangulation/triangulate.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp b/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp index b0a2085..21843d8 100644 --- a/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp +++ b/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp @@ -3,6 +3,7 @@ #include #include +#include // memory header is required on linux, and not on macos #include #include #include From bf0e43c0baacc49eeda98390e9a018c552889a1a Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Tue, 28 Jan 2025 20:58:01 +0100 Subject: [PATCH 5/7] disable debug mode --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5a758a3..5918c8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ sdist.include = ["src/PartSegCore_compiled_backend/version.py"] cmake.version = ">=3.21" sdist.exclude = [".github", "tox.ini", "build_utils", "notebooks", ".readthedocs.yaml"] wheel.exclude = ["**.pyx"] -cmake.build-type = "Debug" +#cmake.build-type = "Debug" [project] From c484ecc6884e33957ad05e3e238e06f348b03c2f Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Tue, 28 Jan 2025 21:28:18 +0100 Subject: [PATCH 6/7] improve documentation --- .../triangulation/triangulate.hpp | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp b/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp index 21843d8..374c766 100644 --- a/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp +++ b/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp @@ -4,7 +4,6 @@ #include #include #include // memory header is required on linux, and not on macos -#include #include #include #include @@ -1288,12 +1287,22 @@ inline PathTriangulation triangulate_path_edge( return result; } +/** + * Represents an edge in a graph structure used for polygon processing. + * Each edge contains a reference to its opposite point and a flag to track + * if it has been visited during graph traversal. + */ struct GraphEdge { point::Point opposite_point; bool visited; explicit GraphEdge(point::Point p) : opposite_point(p), visited(false) {} }; +/** + * Represents a node in a graph structure used for polygon processing. + * Each node contains its edges, a sub-index for traversal tracking, + * and a visited flag for graph traversal. + */ struct GraphNode { std::vector edges; std::size_t sub_index; @@ -1302,10 +1311,23 @@ struct GraphNode { GraphNode() : sub_index(0), visited(false) {} }; +/** + * Splits a polygon into sub-polygons by identifying and removing edges that + * appear more than once in the polygon's edge list. + * + * This function processes the given polygon and separates it wherever an + * edge is repeated. It generates a collection of sub-polygons such that each + * resulting sub-polygon contains unique edges. This operation can help to + * resolve ambiguities in complex or self-intersecting polygons. + * + * @param polygon The input polygon represented as a list of edges. + * + * @return A vector of sub-polygons, where each sub-polygon is free of repeated + * edges. + */ inline std::vector> split_polygon_on_repeated_edges( const std::vector &polygon) { auto edges_dedup = calc_dedup_edges({polygon}); - std::vector visited(polygon.size(), false); std::vector> result; point::Segment segment; From cb785c6dcbf3eb8fe5836117784ad7c4b8f8d5ec Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Wed, 29 Jan 2025 09:49:42 +0100 Subject: [PATCH 7/7] fix traingle numbers --- src/PartSegCore_compiled_backend/triangulate.pyx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PartSegCore_compiled_backend/triangulate.pyx b/src/PartSegCore_compiled_backend/triangulate.pyx index db71655..12b598a 100644 --- a/src/PartSegCore_compiled_backend/triangulate.pyx +++ b/src/PartSegCore_compiled_backend/triangulate.pyx @@ -436,6 +436,7 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray], split_e cdef pair[vector[Triangle], vector[Point]] triangulation_result cdef vector[PathTriangulation] edge_result cdef cnp.ndarray[cnp.uint32_t, ndim=2] triangles, edge_triangles + cdef size_t triangle_count = 0 cdef cnp.ndarray[cnp.float32_t, ndim=2] points, edge_offsets, edges_centers, polygon cdef size_t i, j, len_path, edge_triangle_count, edge_center_count, edge_triangle_index, edge_center_index @@ -475,7 +476,7 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray], split_e triangles[i, 1] = triangulation_result.first[i].y triangles[i, 2] = triangulation_result.first[i].z - points =np.empty((triangulation_result.second.size(), 2), dtype=np.float32) + points = np.empty((triangulation_result.second.size(), 2), dtype=np.float32) for i in range(triangulation_result.second.size()): points[i, 0] = triangulation_result.second[i].x points[i, 1] = triangulation_result.second[i].y @@ -494,10 +495,11 @@ def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray], split_e edge_center_index = 0 for i in range(edge_result.size()): for j in range(edge_result[i].triangles.size()): - edge_triangles[edge_triangle_index, 0] = edge_result[i].triangles[j].x - edge_triangles[edge_triangle_index, 1] = edge_result[i].triangles[j].y - edge_triangles[edge_triangle_index, 2] = edge_result[i].triangles[j].z + edge_triangles[edge_triangle_index, 0] = edge_result[i].triangles[j].x + triangle_count + edge_triangles[edge_triangle_index, 1] = edge_result[i].triangles[j].y + triangle_count + edge_triangles[edge_triangle_index, 2] = edge_result[i].triangles[j].z + triangle_count edge_triangle_index += 1 + triangle_count += edge_result[i].centers.size() for j in range(edge_result[i].centers.size()): edges_centers[edge_center_index, 0] = edge_result[i].centers[j].x