PartSegCore-compiled-backend 0.15.10__tar.gz → 0.15.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/.pre-commit-config.yaml +2 -2
  2. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/PKG-INFO +30 -5
  3. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/Readme.md +25 -0
  4. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/multiscale_opening/mso.h +1 -1
  5. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/triangulation/intersection.hpp +23 -15
  6. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/triangulation/point.hpp +13 -0
  7. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/triangulation/triangulate.hpp +67 -11
  8. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/version.py +9 -4
  9. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/tests/test_triangulate.py +53 -0
  10. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/.gitignore +0 -0
  11. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/CMakeLists.txt +0 -0
  12. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/LICENSE.txt +0 -0
  13. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/MANIFEST.in +0 -0
  14. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/docs/Makefile +0 -0
  15. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/docs/calc_bounds.rst +0 -0
  16. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/docs/conf.py +0 -0
  17. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/docs/fast_unique.rst +0 -0
  18. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/docs/index.rst +0 -0
  19. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/docs/make.bat +0 -0
  20. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/docs/napari_mapping.rst +0 -0
  21. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/docs/sprawl_utils.rst +0 -0
  22. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/pyproject.toml +0 -0
  23. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/__init__.py +0 -0
  24. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/_fast_unique.pyi +0 -0
  25. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/_fast_unique.pyx +0 -0
  26. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/_napari_mapping.pyx +0 -0
  27. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/calc_bounds.pyi +0 -0
  28. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/calc_bounds.pyx +0 -0
  29. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/color_image_cython.pyi +0 -0
  30. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/color_image_cython.pyx +0 -0
  31. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/fast_unique.py +0 -0
  32. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/multiscale_opening/__init__.py +0 -0
  33. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/multiscale_opening/mso_bind.pyi +0 -0
  34. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/multiscale_opening/mso_bind.pyx +0 -0
  35. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/multiscale_opening/my_queue.h +0 -0
  36. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/napari_mapping.py +0 -0
  37. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/__init__.py +0 -0
  38. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/distance_utils.pxd +0 -0
  39. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/euclidean_cython.pyi +0 -0
  40. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/euclidean_cython.pyx +0 -0
  41. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/find_split.py +0 -0
  42. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/find_split.pyi +0 -0
  43. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/fuzzy_distance.pyx +0 -0
  44. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/global_consts.h +0 -0
  45. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/my_queue.h +0 -0
  46. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/path_sprawl_cython.pyi +0 -0
  47. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/path_sprawl_cython.pyx +0 -0
  48. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/put_borders_in_queue.pyx +0 -0
  49. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/sprawl_utils.pyi +0 -0
  50. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/sprawl_utils/sprawl_utils.pyx +0 -0
  51. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/triangulate.pyx +0 -0
  52. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/triangulation/debug_util.hpp +0 -0
  53. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/PartSegCore_compiled_backend/utils.py +0 -0
  54. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/tests/test_calc_bounds.py +0 -0
  55. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/tests/test_coloring.py +0 -0
  56. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/tests/test_fast_unique.py +0 -0
  57. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/tests/test_napari_mapping.py +0 -0
  58. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/tests/test_sprawl_utils/__init__.py +0 -0
  59. {partsegcore_compiled_backend-0.15.10 → partsegcore_compiled_backend-0.15.12}/src/tests/test_sprawl_utils/test_euclidean.py +0 -0
@@ -4,7 +4,7 @@ default_language_version:
4
4
  repos:
5
5
  - repo: https://github.com/astral-sh/ruff-pre-commit
6
6
  # Ruff version.
7
- rev: v0.9.2
7
+ rev: v0.9.10
8
8
  hooks:
9
9
  # Run the linter.
10
10
  - id: ruff
@@ -30,7 +30,7 @@ repos:
30
30
  hooks:
31
31
  - id: markdownlint
32
32
  - repo: https://github.com/codespell-project/codespell
33
- rev: v2.3.0
33
+ rev: v2.4.1
34
34
  hooks:
35
35
  - id: codespell
36
36
  additional_dependencies:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: PartSegCore_compiled_backend
3
- Version: 0.15.10
3
+ Version: 0.15.12
4
4
  Summary: PartSegCore compiled backend,
5
5
  Keywords: bioimaging
6
6
  Author-Email: Grzegorz Bokota <g.bokota@cent.uw.edu.pl>
@@ -18,13 +18,13 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
18
18
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
19
19
  Project-URL: Homepage, https://4dnucleome.cent.uw.edu.pl/PartSeg/
20
20
  Project-URL: Documentation, https://partsegcore-compiled-backend.readthedocs.io/en/stable/
21
- Project-URL: Source code, https://github.com/4DNucleome/PartSegCore-compiled-backend
21
+ Project-URL: Source Code, https://github.com/4DNucleome/PartSegCore-compiled-backend
22
22
  Requires-Python: >=3.9
23
23
  Requires-Dist: numpy>=1.23.5
24
- Requires-Dist: pytest>=7.0.0; extra == "test"
25
- Requires-Dist: sphinx>=5.0.0; extra == "docs"
26
24
  Provides-Extra: test
25
+ Requires-Dist: pytest>=7.0.0; extra == "test"
27
26
  Provides-Extra: docs
27
+ Requires-Dist: sphinx>=5.0.0; extra == "docs"
28
28
  Description-Content-Type: text/markdown
29
29
 
30
30
  # PartSegCore_compiled_backend
@@ -61,6 +61,31 @@ pip install .
61
61
  As currently there is no wheel for macOS ARM64
62
62
  it is required to build package from source.
63
63
 
64
+ ## Development in Clion
65
+
66
+ To install Python build dependencies, run:
67
+
68
+ ```bash
69
+ pip install scikit-build-core cython cython-cmake numpy setuptools-scm
70
+ ```
71
+
72
+ Then, install the package in development mode:
73
+
74
+ ```bash
75
+ pip install --no-build-isolation --config-settings=editable.rebuild=true\
76
+ -Cbuild-dir=build -ve .
77
+ ```
78
+
79
+ vendor `FindCython.cmake` from cython-cmake to `cmake` directory.
80
+
81
+ ```bash
82
+ mkdir cmake && cython-cmake vendor cmake
83
+ ```
84
+
85
+ Set `CUSTOM_MODULE_PATH` environment variable to full path to cmake directory.
86
+
87
+ Set it in Settings → Build, Execution, Deployment → CMake → Environment
88
+
64
89
  ## Cite as
65
90
 
66
91
  Bokota, G., Sroka, J., Basu, S. et al. PartSeg: a tool for quantitative feature extraction
@@ -32,6 +32,31 @@ pip install .
32
32
  As currently there is no wheel for macOS ARM64
33
33
  it is required to build package from source.
34
34
 
35
+ ## Development in Clion
36
+
37
+ To install Python build dependencies, run:
38
+
39
+ ```bash
40
+ pip install scikit-build-core cython cython-cmake numpy setuptools-scm
41
+ ```
42
+
43
+ Then, install the package in development mode:
44
+
45
+ ```bash
46
+ pip install --no-build-isolation --config-settings=editable.rebuild=true\
47
+ -Cbuild-dir=build -ve .
48
+ ```
49
+
50
+ vendor `FindCython.cmake` from cython-cmake to `cmake` directory.
51
+
52
+ ```bash
53
+ mkdir cmake && cython-cmake vendor cmake
54
+ ```
55
+
56
+ Set `CUSTOM_MODULE_PATH` environment variable to full path to cmake directory.
57
+
58
+ Set it in Settings → Build, Execution, Deployment → CMake → Environment
59
+
35
60
  ## Cite as
36
61
 
37
62
  Bokota, G., Sroka, J., Basu, S. et al. PartSeg: a tool for quantitative feature extraction
@@ -580,7 +580,7 @@ class MSO {
580
580
  queue_copy = queue;
581
581
  // std::cerr << "# Queue1 " << queue.get_size() << " Queue2 " <<
582
582
  // queue_copy.get_size() << std::endl;
583
- // Calculate area which can be reached by monoticall path (firs part of
583
+ // Calculate area which can be reached by monoticall path (first part of
584
584
  // Morphological neighborhood)
585
585
  while (!queue.empty()) {
586
586
  coord = queue.front();
@@ -112,6 +112,12 @@ inline int64_t double_as_hex(double d) {
112
112
  return result;
113
113
  }
114
114
 
115
+ enum Orientation {
116
+ COLLINEAR = 0,
117
+ CLOCKWISE = 1,
118
+ COUNTERCLOCKWISE = 2,
119
+ };
120
+
115
121
  /**
116
122
  * Determines the orientation of the triplet (p, q, r).
117
123
  *
@@ -119,12 +125,14 @@ inline int64_t double_as_hex(double d) {
119
125
  * @param q The second point.
120
126
  * @param r The third point.
121
127
  *
122
- * @return 0 if p, q and r are collinear.
123
- * 1 if the triplet (p, q, r) is in a clockwise orientation.
124
- * 2 if the triplet (p, q, r) is in a counterclockwise orientation.
128
+ * @return Orientation::COLLINEAR if p, q and r are collinear.
129
+ * Orientation::CLOCKWISE if the triplet (p, q, r) is in a clockwise
130
+ * orientation.
131
+ * Orientation::COUNTERCLOCKWISE if the triplet (p, q, r) is in a
132
+ * counterclockwise orientation.
125
133
  */
126
- inline int _orientation(const point::Point &p, const point::Point &q,
127
- const point::Point &r) {
134
+ inline Orientation _orientation(const point::Point &p, const point::Point &q,
135
+ const point::Point &r) {
128
136
  double val1 = ((q.y - p.y) * (r.x - q.x));
129
137
  double val2 = ((r.y - q.y) * (q.x - p.x));
130
138
  // This commented code if for debugging purposes of differences between macOS
@@ -142,8 +150,8 @@ inline int _orientation(const point::Point &p, const point::Point &q,
142
150
  // }
143
151
  // Instead of using classical equation, we need to use two variables
144
152
  // to handle problem with strange behavior on macOS.
145
- if (val1 == val2) return 0;
146
- return (val1 > val2) ? 1 : 2;
153
+ if (val1 == val2) return Orientation::COLLINEAR;
154
+ return (val1 > val2) ? Orientation::CLOCKWISE : Orientation::COUNTERCLOCKWISE;
147
155
  }
148
156
 
149
157
  /**
@@ -163,17 +171,17 @@ inline bool _do_intersect(const point::Segment &s1, const point::Segment &s2) {
163
171
  const point::Point &p2 = s2.bottom;
164
172
  const point::Point &q2 = s2.top;
165
173
 
166
- int o1 = _orientation(p1, q1, p2);
167
- int o2 = _orientation(p1, q1, q2);
168
- int o3 = _orientation(p2, q2, p1);
169
- int o4 = _orientation(p2, q2, q1);
174
+ Orientation o1 = _orientation(p1, q1, p2);
175
+ Orientation o2 = _orientation(p1, q1, q2);
176
+ Orientation o3 = _orientation(p2, q2, p1);
177
+ Orientation o4 = _orientation(p2, q2, q1);
170
178
 
171
179
  if (o1 != o2 && o3 != o4) return true;
172
180
 
173
- if (o1 == 0 && _on_segment(p1, p2, q1)) return true;
174
- if (o2 == 0 && _on_segment(p1, q2, q1)) return true;
175
- if (o3 == 0 && _on_segment(p2, p1, q2)) return true;
176
- if (o4 == 0 && _on_segment(p2, q1, q2)) return true;
181
+ if (o1 == Orientation::COLLINEAR && _on_segment(p1, p2, q1)) return true;
182
+ if (o2 == Orientation::COLLINEAR && _on_segment(p1, q2, q1)) return true;
183
+ if (o3 == Orientation::COLLINEAR && _on_segment(p2, p1, q2)) return true;
184
+ if (o4 == Orientation::COLLINEAR && _on_segment(p2, q1, q2)) return true;
177
185
 
178
186
  return false;
179
187
  }
@@ -208,6 +208,19 @@ struct Segment {
208
208
  }
209
209
  };
210
210
  };
211
+ Point centroid(const std::vector<Point> &point_list) {
212
+ if (point_list.empty()) {
213
+ return {0, 0};
214
+ }
215
+ Point res(0, 0);
216
+ for (auto &point : point_list) {
217
+ res.x += point.x;
218
+ res.y += point.y;
219
+ }
220
+ res.x /= float(point_list.size());
221
+ res.y /= float(point_list.size());
222
+ return res;
223
+ }
211
224
  } // namespace partsegcore::point
212
225
 
213
226
  // overload of hash function for
@@ -2,6 +2,7 @@
2
2
  #define PARTSEGCORE_TRIANGULATE_H
3
3
 
4
4
  #include <algorithm>
5
+ #include <cmath>
5
6
  #include <map>
6
7
  #include <memory> // memory header is required on linux, and not on macos
7
8
  #include <set>
@@ -602,6 +603,36 @@ struct MonotonePolygonBuilder {
602
603
  };
603
604
  };
604
605
 
606
+ /*
607
+ * Check if the polygon, that all angles have the same orientation
608
+ * do not have self-intersections
609
+ *
610
+ * @param begin Iterator to the first point of the polygon
611
+ * @param end Iterator to the end of the polygon
612
+ * @param centroid Centroid of the polygon
613
+ *
614
+ * @return True if the polygon is simple, false otherwise
615
+ */
616
+ template <typename Iterator>
617
+ bool is_simple_polygon(Iterator begin, Iterator end, point::Point centroid) {
618
+ double start_angle = std::atan2(begin->y - centroid.y, begin->x - centroid.x);
619
+ double prev_angle = 0;
620
+ begin++;
621
+ for (; begin != end; begin++) {
622
+ double angle =
623
+ std::atan2(begin->y - centroid.y, begin->x - centroid.x) - start_angle;
624
+ if (angle < 0) {
625
+ angle += 2 * M_PI;
626
+ }
627
+ if (angle < prev_angle) {
628
+ return false;
629
+ } else {
630
+ prev_angle = angle;
631
+ }
632
+ }
633
+ return true;
634
+ }
635
+
605
636
  /**
606
637
  * Checks if a given polygon is convex.
607
638
  *
@@ -615,26 +646,47 @@ struct MonotonePolygonBuilder {
615
646
  * @return True if the polygon is convex, false otherwise.
616
647
  */
617
648
  inline bool _is_convex(const std::vector<point::Point> &polygon) {
618
- int orientation = 0;
619
- int triangle_orientation;
620
- for (std::size_t i = 0; i < polygon.size() - 2; i++) {
621
- triangle_orientation =
622
- intersection::_orientation(polygon[i], polygon[i + 1], polygon[i + 2]);
623
- if (triangle_orientation == 0) continue;
624
- if (orientation == 0)
649
+ if (polygon.size() < 3) return false;
650
+ if (polygon.size() == 3) return true;
651
+ intersection::Orientation orientation = intersection::Orientation::COLLINEAR;
652
+ intersection::Orientation triangle_orientation;
653
+ size_t idx = 0;
654
+ for (; idx < polygon.size() - 2; idx++) {
655
+ triangle_orientation = intersection::_orientation(
656
+ polygon[idx], polygon[(idx + 1)], polygon[(idx + 2)]);
657
+ if (triangle_orientation != intersection::Orientation::COLLINEAR) {
625
658
  orientation = triangle_orientation;
626
- else if (orientation != triangle_orientation)
659
+ break;
660
+ }
661
+ }
662
+ if (orientation == intersection::Orientation::COLLINEAR) {
663
+ return false;
664
+ }
665
+ for (; idx < polygon.size() - 2; idx++) {
666
+ triangle_orientation = intersection::_orientation(
667
+ polygon[idx], polygon[(idx + 1)], polygon[(idx + 2)]);
668
+ if (triangle_orientation != 0 && triangle_orientation != orientation) {
627
669
  return false;
670
+ }
628
671
  }
629
672
  triangle_orientation = intersection::_orientation(
630
673
  polygon[polygon.size() - 2], polygon[polygon.size() - 1], polygon[0]);
631
- if (triangle_orientation != 0 && triangle_orientation != orientation)
674
+ if (triangle_orientation != 0 && triangle_orientation != orientation) {
632
675
  return false;
676
+ }
633
677
  triangle_orientation = intersection::_orientation(polygon[polygon.size() - 1],
634
678
  polygon[0], polygon[1]);
635
- if (triangle_orientation != 0 && triangle_orientation != orientation)
679
+ if (triangle_orientation != 0 && triangle_orientation != orientation) {
636
680
  return false;
637
- return true;
681
+ }
682
+
683
+ point::Point centroid = point::centroid(polygon);
684
+
685
+ if (orientation == intersection::Orientation::COUNTERCLOCKWISE) {
686
+ return is_simple_polygon(polygon.begin(), polygon.end(), centroid);
687
+ } else {
688
+ return is_simple_polygon(polygon.rbegin(), polygon.rend(), centroid);
689
+ }
638
690
  }
639
691
 
640
692
  /**
@@ -1320,6 +1372,9 @@ struct GraphNode {
1320
1372
  * resulting sub-polygon contains unique edges. This operation can help to
1321
1373
  * resolve ambiguities in complex or self-intersecting polygons.
1322
1374
  *
1375
+ * Note: Polygons with exactly 2 points are treated as single line segments and
1376
+ * are not processed for edge deduplication between their first and last points.
1377
+ *
1323
1378
  * @param polygon The input polygon represented as a list of edges.
1324
1379
  *
1325
1380
  * @return A vector of sub-polygons, where each sub-polygon is free of repeated
@@ -1327,6 +1382,7 @@ struct GraphNode {
1327
1382
  */
1328
1383
  inline std::vector<std::vector<point::Point>> split_polygon_on_repeated_edges(
1329
1384
  const std::vector<point::Point> &polygon) {
1385
+ if (polygon.size() < 3) return {polygon};
1330
1386
  auto edges_dedup = calc_dedup_edges({polygon});
1331
1387
  std::vector<std::vector<point::Point>> result;
1332
1388
  point::Segment segment;
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.15.10'
16
- __version_tuple__ = version_tuple = (0, 15, 10)
20
+ __version__ = version = '0.15.12'
21
+ __version_tuple__ = version_tuple = (0, 15, 12)
@@ -208,6 +208,7 @@ def test_triangle_convex_polygon():
208
208
  ],
209
209
  )
210
210
  def test_triangulate_polygon_py_convex(polygon, expected):
211
+ assert is_convex(polygon)
211
212
  assert triangulate_polygon_py(polygon)[0] == expected
212
213
 
213
214
 
@@ -691,3 +692,55 @@ def test_splitting_edges(split_edges, triangles):
691
692
  )
692
693
  triangles_ = triangulate_polygon_with_edge_numpy_li([polygon], split_edges=split_edges)[1][2]
693
694
  assert len(triangles_) == triangles
695
+
696
+
697
+ def generate_regular_polygon(n: int, reverse: bool, radius: int = 1) -> np.ndarray:
698
+ angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
699
+ if reverse:
700
+ angles = angles[::-1]
701
+ return np.column_stack((radius * np.cos(angles), radius * np.sin(angles)))
702
+
703
+
704
+ def generate_self_intersecting_polygon(n: int, reverse: bool, radius: int = 1) -> np.ndarray:
705
+ """Generate self-intersecting polygon with n vertices
706
+
707
+ The polygon is generated by doubling the angle range of a regular polygon
708
+ """
709
+ assert n % 2 == 1, 'an odd number is required to generate a self-intersecting polygon'
710
+ angles = np.linspace(0, 4 * np.pi, n, endpoint=False)
711
+ if reverse:
712
+ angles = angles[::-1]
713
+ return np.column_stack((radius * np.cos(angles), radius * np.sin(angles)))
714
+
715
+
716
+ def rotation_matrix(angle: float) -> np.ndarray:
717
+ """Create a 2D rotation matrix for the given angle in degrees."""
718
+ return np.array(
719
+ [
720
+ [np.cos(np.radians(angle)), -np.sin(np.radians(angle))],
721
+ [np.sin(np.radians(angle)), np.cos(np.radians(angle))],
722
+ ]
723
+ )
724
+
725
+
726
+ ANGLES = [0, 5, 75, 95, 355]
727
+
728
+
729
+ @pytest.mark.parametrize('angle', ANGLES, ids=str)
730
+ @pytest.mark.parametrize('n_vertex', [5, 7, 19])
731
+ @pytest.mark.parametrize('reverse', [False, True])
732
+ def test_is_convex_self_intersection(angle, n_vertex, reverse):
733
+ p = generate_self_intersecting_polygon(n_vertex, reverse)
734
+ rot = rotation_matrix(angle)
735
+ data = np.dot(p, rot)
736
+ assert not is_convex(data)
737
+
738
+
739
+ @pytest.mark.parametrize('angle', ANGLES, ids=str)
740
+ @pytest.mark.parametrize('n_vertex', [3, 4, 7, 12, 15, 20])
741
+ @pytest.mark.parametrize('reverse', [False, True])
742
+ def test_is_convex_regular_polygon(angle, n_vertex, reverse):
743
+ poly = generate_regular_polygon(n_vertex, reverse=reverse)
744
+ rot = rotation_matrix(angle)
745
+ rotated_poly = np.dot(poly, rot)
746
+ assert is_convex(rotated_poly)