Skip to content

Conversation

@Czaki
Copy link
Contributor

@Czaki Czaki commented Jan 28, 2025

Summary by CodeRabbit

  • New Features

    • Enhanced polygon triangulation with support for handling repeated edges.
    • Added ability to split polygons with overlapping edges before triangulation.
    • Introduced new structures to facilitate edge and node representation in polygon processing.
  • Tests

    • Added comprehensive test cases for polygon splitting and triangulation scenarios, focusing on handling repeated edges.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2025

Walkthrough

The pull request introduces enhanced polygon triangulation capabilities by adding functionality to handle polygons with repeated edges. A new function split_polygon_on_repeated_edges is implemented in both C++ and Python, which can detect and split polygons with overlapping edges. The triangulate_polygon_with_edge_numpy_li function is updated to include an optional split_edges parameter, allowing users to preprocess polygons before triangulation. This improvement provides more robust handling of complex polygon geometries.

Changes

File Change Summary
src/PartSegCore_compiled_backend/triangulate.pyx - Added split_polygon_on_repeated_edges function
- Added Python wrapper split_polygon_on_repeated_edges_py
- Updated triangulate_polygon_with_edge_numpy_li to support edge splitting
src/PartSegCore_compiled_backend/triangulation/triangulate.hpp - Introduced GraphEdge and GraphNode structs
- Added split_polygon_on_repeated_edges inline function for C++ implementation
src/tests/test_triangulate.py - Added tests for polygon splitting functionality
- Imported new split_polygon_on_repeated_edges_py function

Sequence Diagram

sequenceDiagram
    participant User
    participant TriangulateFn as Triangulate Function
    participant SplitEdgesFn as Split Edges Function
    
    User->>TriangulateFn: Call with polygon and split_edges=True
    TriangulateFn->>SplitEdgesFn: Preprocess polygon
    SplitEdgesFn-->>TriangulateFn: Return split polygons
    TriangulateFn->>TriangulateFn: Triangulate each polygon
    TriangulateFn-->>User: Return triangulation result
Loading

Possibly related PRs

Poem

🐰 A Rabbit's Ode to Polygon Precision

Edges that twist and turns that bend,
Our code now splits where lines transcend!
No polygon too complex to trace,
With triangles we'll fill each space.
Geometry dances, edges unfurl! 🔺


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
src/PartSegCore_compiled_backend/triangulate.pyx (3)

431-431: Add docstring references for the new parameter.

The function gained a new parameter split_edges, but the docstring does not mention it. Consider updating the docstring to clarify its usage and expected behavior.


434-434: Limit scope of edge_split_list to avoid accidental carryover.

Declaring edge_split_list inside the loop might reduce accidental data retention and improve maintainability.

@@ -431,2 +431,3 @@
 def triangulate_polygon_with_edge_numpy_li(polygon_li: list[np.ndarray], split_edges: bool=False):
     cdef vector[Point] polygon_vector
+    cdef vector[vector[Point]] edge_split_list
     cdef vector[vector[Point]] polygon_vector_list

519-535: Clarify behavior for small polygons.

When the input polygon has fewer than two points, this function may return empty or degenerate results. Consider validating input or documenting this behavior.

src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (2)

6-6: Remove unused include if not needed.

#include <queue> does not appear to be referenced. Consider removing it.


1304-1346: Remove unused visited vector.

Line 1307 declares a local visited vector that is never used, which can cause confusion. Also confirm if zero-length polygons need handling.

-  std::vector<bool> visited(polygon.size(), false);
pyproject.toml (1)

12-12: Consider making Debug mode configurable.

Setting cmake.build-type = "Debug" globally could impact performance in production builds. Debug builds:

  • Include debugging symbols
  • Disable optimizations
  • Increase binary size
  • Result in slower runtime performance

Consider one of these approaches:

  1. Use environment variables to control build type
  2. Make it configurable via command-line arguments
  3. Use different settings for development vs. production builds

Here's an example implementation using environment variables:

-cmake.build-type = "Debug"
+cmake.build-type = { env = "CMAKE_BUILD_TYPE", default = "Release" }

This allows developers to enable Debug mode when needed:

CMAKE_BUILD_TYPE=Debug pip install .
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 489f6d4 and 65c3636.

📒 Files selected for processing (4)
  • pyproject.toml (1 hunks)
  • src/PartSegCore_compiled_backend/triangulate.pyx (4 hunks)
  • src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (2 hunks)
  • src/tests/test_triangulate.py (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Test on windows-latest with Python 3.12
  • GitHub Check: Test on windows-latest with Python 3.11
  • GitHub Check: Test on windows-latest with Python 3.10
  • GitHub Check: Test on windows-latest with Python 3.9
🔇 Additional comments (6)
src/PartSegCore_compiled_backend/triangulate.pyx (2)

97-97: Consider adding usage doc or reference to docstring.

Everything looks correct with the function signature. It might be helpful to document it thoroughly in the Python layer for discoverability.


459-466: Double-check concurrency safety under nogil.

Invoking split_polygon_on_repeated_edges and triangulate_path_edge within cython.nogil suggests thread safety. Verify that these routines do not modify shared global state.

✅ Verification successful

Thread safety verified for nogil blocks

The functions split_polygon_on_repeated_edges and triangulate_path_edge are thread-safe. They operate purely on their input parameters and local variables, with no shared mutable state between threads. All vector operations are performed on thread-local instances.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Searching for shared global or thread_local usage in triangulation/*.hpp
ast-grep --pattern $'static $_ = $_;'
ast-grep --pattern $'thread_local $_ = $_;'

Length of output: 83


Script:

#!/bin/bash
# Find relevant source files
echo "=== Searching for relevant source files ==="
fd -e pyx -e pxd -e cpp -e h -e hpp

echo -e "\n=== Searching for function definitions ==="
rg -l "split_polygon_on_repeated_edges|triangulate_path_edge"

echo -e "\n=== Checking for global variables ==="
rg "^[[:space:]]*(static|thread_local|global)\s+\w+"

Length of output: 3527


Script:

#!/bin/bash
# Check implementation details
echo "=== Function implementations in triangulate.pyx ==="
rg -A 10 "def (split_polygon_on_repeated_edges|triangulate_path_edge)" src/PartSegCore_compiled_backend/triangulate.pyx

echo -e "\n=== C++ implementations in triangulate.hpp ==="
rg -A 10 "(split_polygon_on_repeated_edges|triangulate_path_edge)" src/PartSegCore_compiled_backend/triangulation/triangulate.hpp

echo -e "\n=== Vector operations and memory management ==="
rg "vector[<]" src/PartSegCore_compiled_backend/triangulation/triangulate.hpp

Length of output: 6822

src/tests/test_triangulate.py (3)

13-13: No issues with the updated import.

Adding split_polygon_on_repeated_edges_py aligns cleanly with the new functionality.


665-673: Test thoroughly verifies the no-split scenario.

The test checks that a square polygon remains a single outer boundary without splits. Good basic coverage.


684-691: Parametrized test successfully covers both split and non-split modes.

Checking triangle counts for both split_edges=True and split_edges=False ensures robust test coverage.

src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (1)

1290-1302: Simple, clear adjacency structures.

GraphEdge and GraphNode properly capture necessary state for repeated-edge tracking. No immediate issues found.

Comment on lines 675 to 682
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)]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the missing polygon argument.

split_polygon_on_repeated_edges_py() requires a polygon parameter. Calling it without arguments will raise an error.

-def test_split_polygon_on_repeated_edges_py_square_in_square():
-    res = split_polygon_on_repeated_edges_py()
+def test_split_polygon_on_repeated_edges_py_square_in_square():
+    square_in_square = [
+        [0, 0], [0, 1], [1, 1], [1, 0], [0, 0],
+        [0, 1], [1, 1], [1, 0]  # Example: second square overlapping the first
+    ]
+    res = split_polygon_on_repeated_edges_py(square_in_square)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)]
def test_split_polygon_on_repeated_edges_py_square_in_square():
square_in_square = [
[0, 0], [0, 1], [1, 1], [1, 0], [0, 0],
[0, 1], [1, 1], [1, 0] # Example: second square overlapping the first
]
res = split_polygon_on_repeated_edges_py(square_in_square)
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)]

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (4)

1291-1295: Add documentation for the GraphEdge structure.

The structure is well-designed but would benefit from documentation explaining its purpose and member variables.

Add documentation like this:

+/**
+ * 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;  //< The point at the other end of the edge
   bool visited;                 //< Flag to mark if edge has been visited
   explicit GraphEdge(point::Point p) : opposite_point(p), visited(false) {}
 };

1297-1303: Add documentation and consider const correctness.

The structure would benefit from documentation and potential const member functions.

Add documentation and consider these improvements:

+/**
+ * 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<GraphEdge> edges;
   std::size_t sub_index;
   bool visited;
 
   GraphNode() : sub_index(0), visited(false) {}
+  
+  // Consider adding const member functions for querying state
+  bool is_visited() const { return visited; }
+  std::size_t get_sub_index() const { return sub_index; }
+  const std::vector<GraphEdge>& get_edges() const { return edges; }
 };

1312-1323: Consider extracting graph construction into a separate function.

The graph construction logic could be clearer if extracted into a helper function.

Consider refactoring like this:

+inline std::unordered_map<point::Point, GraphNode> build_edges_graph(
+    const std::vector<point::Point> &polygon,
+    const std::unordered_set<point::Segment> &edges_set) {
+  std::unordered_map<point::Point, GraphNode> edges_map;
+  point::Segment segment;
+  
+  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]);
+    }
+  }
+  
+  segment = {polygon.back(), polygon.front()};
+  if (edges_set.count(segment) > 0) {
+    edges_map[polygon.back()].edges.emplace_back(polygon.front());
+  }
+  
+  return edges_map;
+}

1324-1345: Extract polygon construction logic into a separate function.

The polygon construction logic is complex and would benefit from being extracted into a helper function with clearer variable names.

Consider refactoring like this:

+inline std::vector<point::Point> construct_new_polygon(
+    point::Point start_point,
+    GraphNode* start_node,
+    std::unordered_map<point::Point, GraphNode>& edges_map) {
+  std::vector<point::Point> new_polygon;
+  new_polygon.push_back(start_point);
+  
+  auto* current_node = start_node;
+  while (current_node->sub_index < current_node->edges.size()) {
+    auto* prev_node = current_node;
+    auto next_point = current_node->edges[current_node->sub_index].opposite_point;
+    current_node = &edges_map.at(next_point);
+    prev_node->sub_index++;
+    current_node->visited = true;
+    new_polygon.push_back(next_point);
+  }
+  
+  // Remove duplicate points at start/end
+  while (new_polygon.size() > 1 && new_polygon.front() == new_polygon.back()) {
+    new_polygon.pop_back();
+  }
+  
+  return new_polygon;
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17eb20a and 10512a0.

📒 Files selected for processing (1)
  • src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Test on windows-latest with Python 3.12
  • GitHub Check: Test on windows-latest with Python 3.11
  • GitHub Check: Test on windows-latest with Python 3.10
  • GitHub Check: Test on windows-latest with Python 3.9
🔇 Additional comments (1)
src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (1)

6-6: LGTM! Good documentation of platform-specific requirement.

The comment clearly explains why the memory header is required on Linux but not on macOS.

Comment on lines +1305 to +1307
inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
const std::vector<point::Point> &polygon) {
auto edges_dedup = calc_dedup_edges({polygon});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add documentation for the function and validate input.

The function lacks documentation explaining its purpose, algorithm, and parameters. It also needs input validation.

Add documentation and input validation:

+/**
+ * Splits a polygon into multiple polygons where edges are repeated.
+ * 
+ * This function processes a polygon and splits it into multiple polygons
+ * at points where edges are repeated. It uses a graph-based approach
+ * where each point becomes a node and edges are used to traverse and
+ * construct the resulting polygons.
+ *
+ * @param polygon Vector of points representing the input polygon
+ * @return Vector of polygons (each a vector of points) after splitting
+ * @throws std::invalid_argument if polygon has less than 3 points
+ */
 inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
     const std::vector<point::Point> &polygon) {
+  if (polygon.size() < 3) {
+    throw std::invalid_argument("Polygon must have at least 3 points");
+  }
   auto edges_dedup = calc_dedup_edges({polygon});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
const std::vector<point::Point> &polygon) {
auto edges_dedup = calc_dedup_edges({polygon});
/**
* Splits a polygon into multiple polygons where edges are repeated.
*
* This function processes a polygon and splits it into multiple polygons
* at points where edges are repeated. It uses a graph-based approach
* where each point becomes a node and edges are used to traverse and
* construct the resulting polygons.
*
* @param polygon Vector of points representing the input polygon
* @return Vector of polygons (each a vector of points) after splitting
* @throws std::invalid_argument if polygon has less than 3 points
*/
inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
const std::vector<point::Point> &polygon) {
if (polygon.size() < 3) {
throw std::invalid_argument("Polygon must have at least 3 points");
}
auto edges_dedup = calc_dedup_edges({polygon});

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (1)

1301-1313: Consider adding const member functions for better const correctness.

The GraphNode structure is well-documented and designed. Consider adding const member functions if you need to access the node's state in a const context.

Apply this diff to improve const correctness:

 struct GraphNode {
   std::vector<GraphEdge> edges;
   std::size_t sub_index;
   bool visited;

   GraphNode() : sub_index(0), visited(false) {}
+  
+  [[nodiscard]] bool is_visited() const { return visited; }
+  [[nodiscard]] std::size_t get_sub_index() const { return sub_index; }
+  [[nodiscard]] const std::vector<GraphEdge>& get_edges() const { return edges; }
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10512a0 and c484ecc.

📒 Files selected for processing (1)
  • src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Test on windows-latest with Python 3.12
  • GitHub Check: Test on windows-latest with Python 3.11
  • GitHub Check: Test on windows-latest with Python 3.10
  • GitHub Check: Test on windows-latest with Python 3.9
🔇 Additional comments (3)
src/PartSegCore_compiled_backend/triangulation/triangulate.hpp (3)

6-6: LGTM! Clear documentation of platform-specific requirement.

The memory header inclusion with its explanatory comment helps maintain cross-platform compatibility.


1290-1300: LGTM! Well-documented and properly designed structure.

The GraphEdge structure is well-documented and follows good design practices with proper member initialization.


1314-1327: Add documentation for edge cases and exceptions.

The function documentation should include information about:

  • Edge cases (empty polygon, single point, etc.)
  • Exceptions that may be thrown
  • Return value for edge cases

Comment on lines +1328 to +1369
inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
const std::vector<point::Point> &polygon) {
auto edges_dedup = calc_dedup_edges({polygon});
std::vector<std::vector<point::Point>> result;
point::Segment segment;

std::unordered_set edges_set(edges_dedup.begin(), edges_dedup.end());
std::unordered_map<point::Point, GraphNode> 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]);
}
}
segment = {polygon.back(), polygon.front()};
if (edges_set.count(segment) > 0) {
edges_map[polygon.back()].edges.emplace_back(polygon.front());
}
for (auto &edge : edges_map) {
if (edge.second.visited) {
continue;
}
edge.second.visited = true;
std::vector<point::Point> 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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against potential undefined behavior in map access.

The at() function on line 1358 could throw an out_of_range exception if the point is not found in the map. Consider using find() to safely handle this case.

Apply this diff to fix the issue:

-      current_edge = &edges_map.at(next_point);
+      auto it = edges_map.find(next_point);
+      if (it == edges_map.end()) {
+        throw std::runtime_error("Invalid polygon: disconnected edge detected");
+      }
+      current_edge = &it->second;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
const std::vector<point::Point> &polygon) {
auto edges_dedup = calc_dedup_edges({polygon});
std::vector<std::vector<point::Point>> result;
point::Segment segment;
std::unordered_set edges_set(edges_dedup.begin(), edges_dedup.end());
std::unordered_map<point::Point, GraphNode> 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]);
}
}
segment = {polygon.back(), polygon.front()};
if (edges_set.count(segment) > 0) {
edges_map[polygon.back()].edges.emplace_back(polygon.front());
}
for (auto &edge : edges_map) {
if (edge.second.visited) {
continue;
}
edge.second.visited = true;
std::vector<point::Point> 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;
}
inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
const std::vector<point::Point> &polygon) {
auto edges_dedup = calc_dedup_edges({polygon});
std::vector<std::vector<point::Point>> result;
point::Segment segment;
std::unordered_set edges_set(edges_dedup.begin(), edges_dedup.end());
std::unordered_map<point::Point, GraphNode> 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]);
}
}
segment = {polygon.back(), polygon.front()};
if (edges_set.count(segment) > 0) {
edges_map[polygon.back()].edges.emplace_back(polygon.front());
}
for (auto &edge : edges_map) {
if (edge.second.visited) {
continue;
}
edge.second.visited = true;
std::vector<point::Point> 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;
auto it = edges_map.find(next_point);
if (it == edges_map.end()) {
throw std::runtime_error("Invalid polygon: disconnected edge detected");
}
current_edge = &it->second;
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;
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/PartSegCore_compiled_backend/triangulate.pyx (1)

523-537: Enhance the Python wrapper with validation and documentation.

The function would benefit from input validation, better type hints, and a more detailed docstring.

-def split_polygon_on_repeated_edges_py(polygon: Sequence[Sequence[float]]) -> list[list[tuple[float, float]]]:
-    """ Split polygon on repeated edges"""
+def split_polygon_on_repeated_edges_py(
+    polygon: Sequence[Sequence[float]],
+) -> list[list[tuple[float, float]]]:
+    """Split a polygon into multiple polygons where edges are repeated.
+
+    Parameters
+    ----------
+    polygon : Sequence[Sequence[float]]
+        Input polygon as a sequence of 2D points (x, y coordinates).
+        Each point should be a sequence of two float values.
+
+    Returns
+    -------
+    list[list[tuple[float, float]]]
+        List of polygons, where each polygon is a list of points.
+        Each point is a tuple of (x, y) coordinates.
+
+    Raises
+    ------
+    ValueError
+        If the polygon has less than 3 points or contains invalid coordinates.
+    """
+    if len(polygon) < 3:
+        raise ValueError("Polygon must have at least 3 points")
+    if not all(len(p) == 2 for p in polygon):
+        raise ValueError("All points must have exactly 2 coordinates")
+
     cdef vector[Point] polygon_vector
     cdef vector[vector[Point]] result
     cdef Point p1, p2
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c484ecc and cb785c6.

📒 Files selected for processing (1)
  • src/PartSegCore_compiled_backend/triangulate.pyx (6 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Test on macos-latest with Python 3.9
  • GitHub Check: Test on windows-latest with Python 3.12
  • GitHub Check: Test on windows-latest with Python 3.11
  • GitHub Check: Test on windows-latest with Python 3.10
  • GitHub Check: Test on windows-latest with Python 3.9
🔇 Additional comments (4)
src/PartSegCore_compiled_backend/triangulate.pyx (4)

97-97: LGTM! Well-defined C++ function declaration.

The declaration follows Cython best practices with proper exception handling and GIL release.


431-431: LGTM! Clear function signature update.

The new split_edges parameter is well-documented and follows the function's naming convention.


498-502: ⚠️ Potential issue

Fix potential off-by-one error in triangle indexing.

The triangle_count variable is updated after using it for offsetting triangle indices, which could lead to incorrect indexing. Consider updating it before processing the triangles.

 for i in range(edge_result.size()):
+    triangle_count = sum(edge_result[k].centers.size() for k in range(i))
     for j in range(edge_result[i].triangles.size()):
         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()

Likely invalid or redundant comment.


Line range hint 431-537: Verify test coverage and usage of the new feature.

Please ensure that:

  1. The new edge splitting functionality is covered by unit tests
  2. The split_edges parameter is used appropriately in the codebase
✅ Verification successful

Test coverage is comprehensive and properly validates the new functionality

The new edge splitting feature is well-tested with:

  • Basic polygon triangulation
  • No-split scenarios
  • Complex split cases (square in square)
  • Explicit verification of split_edges parameter behavior with expected triangle counts
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for test coverage of the new functionality
echo "Checking test coverage..."
rg -l "split_polygon_on_repeated_edges" "src/tests/"

# Check for usage of the new parameter
echo "Checking usage of split_edges parameter..."
rg "triangulate_polygon_with_edge_numpy_li\([^)]*split_edges\s*=\s*True"

Length of output: 311


Script:

#!/bin/bash
# Check test content
echo "Checking test content..."
rg -A 10 -B 10 "split_polygon_on_repeated_edges" "src/tests/test_triangulate.py"

# Look for potential indirect usage
echo "Checking for indirect usage..."
rg "triangulate_polygon_with_edge_numpy_li" -A 3 -B 3

Length of output: 4104

Comment on lines +460 to +467
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for edge splitting.

The edge splitting logic should handle potential error cases, such as empty polygons or invalid splits.

     if split_edges:
         with cython.nogil:
             edge_split_list = split_polygon_on_repeated_edges(polygon_vector)
+            if edge_split_list.empty():
+                # Fall back to original polygon if splitting fails
+                edge_result.push_back(triangulate_path_edge(polygon_vector, True, 3.0, False))
+            else:
                 for edge_li in edge_split_list:
                     edge_result.push_back(triangulate_path_edge(edge_li, True, 3.0, False))
     else:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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))
if split_edges:
with cython.nogil:
edge_split_list = split_polygon_on_repeated_edges(polygon_vector)
if edge_split_list.empty():
# Fall back to original polygon if splitting fails
edge_result.push_back(triangulate_path_edge(polygon_vector, True, 3.0, False))
else:
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))

@Czaki Czaki merged commit 11772a3 into master Feb 13, 2025
22 checks passed
@Czaki Czaki deleted the improve_edges branch February 13, 2025 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants