Skip to content

Commit 21314e3

Browse files
committed
Fix OGC extraction producing figure-8 contours at shared vertices
When hull contours self-touch at pinch points (e.g. exterior and hole sharing a vertex), extract_ogc now detects and splits them inline during pass 1. Split sub-contours are classified by winding area as either new shapes or pending holes, which are joined after pass 2. - Add find_pinch_point using i_key_sort's sort_by_two_keys for O(n log n) duplicate detection on contour points - Add split_all_pinch_points to recursively decompose figure-8 contours - Add comprehensive tests (16 cases) covering 4 geometries × 4 fill rules, validated against Shapely reference output
1 parent 5df4813 commit 21314e3

2 files changed

Lines changed: 412 additions & 91 deletions

File tree

iOverlay/src/core/extract_ogc.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use crate::core::overlay_rule::OverlayRule;
99
use crate::geom::v_segment::VSegment;
1010
use alloc::vec;
1111
use alloc::vec::Vec;
12+
use i_float::int::point::IntPoint;
13+
use i_key_sort::sort::two_keys::TwoKeysSort;
14+
use i_shape::int::path::ContourExtension;
1215
use i_shape::int::shape::IntShapes;
1316
use i_shape::util::reserve::Reserve;
1417

@@ -24,6 +27,8 @@ impl OverlayGraph<'_> {
2427

2528
buffer.points.reserve_capacity(buffer.visited.len());
2629
let mut avg_holes_count = 0;
30+
let mut pending_holes: Vec<Vec<IntPoint>> = Vec::new();
31+
let mut point_buf = Vec::new();
2732

2833
let mut link_index = 0;
2934
while link_index < buffer.visited.len() {
@@ -74,7 +79,19 @@ impl OverlayGraph<'_> {
7479
}
7580

7681
let contour = buffer.points.as_slice().to_vec();
77-
shapes.push(vec![contour]);
82+
if find_pinch_point(&contour, &mut point_buf).is_some() {
83+
for part in split_all_pinch_points(contour, &mut point_buf) {
84+
let area = part.unsafe_area();
85+
let is_hole = if is_main_dir_cw { area < 0 } else { area > 0 };
86+
if is_hole {
87+
pending_holes.push(part);
88+
} else {
89+
shapes.push(vec![part]);
90+
}
91+
}
92+
} else {
93+
shapes.push(vec![contour]);
94+
}
7895
}
7996

8097
if avg_holes_count > 0 {
@@ -157,6 +174,10 @@ impl OverlayGraph<'_> {
157174
shapes.join_sorted_holes(holes, anchors, is_main_dir_cw);
158175
}
159176

177+
if !pending_holes.is_empty() {
178+
shapes.join_unsorted_holes(pending_holes, is_main_dir_cw);
179+
}
180+
160181
shapes
161182
}
162183

@@ -193,3 +214,43 @@ impl OverlayGraph<'_> {
193214
}
194215
}
195216
}
217+
218+
fn find_pinch_point(contour: &[IntPoint], point_buf: &mut Vec<IntPoint>) -> Option<(usize, usize)> {
219+
let n = contour.len();
220+
if n < 2 {
221+
return None;
222+
}
223+
point_buf.clear();
224+
point_buf.extend_from_slice(contour);
225+
point_buf.sort_by_two_keys(false, |p| p.x, |p| p.y);
226+
for w in point_buf.windows(2) {
227+
if w[0] == w[1] {
228+
let target = w[0];
229+
let i = contour.iter().position(|p| *p == target).unwrap();
230+
let j = contour[i + 1..].iter().position(|p| *p == target).unwrap() + i + 1;
231+
return Some((i, j));
232+
}
233+
}
234+
None
235+
}
236+
237+
fn split_all_pinch_points(contour: Vec<IntPoint>, point_buf: &mut Vec<IntPoint>) -> Vec<Vec<IntPoint>> {
238+
if let Some((i, j)) = find_pinch_point(&contour, point_buf) {
239+
let inner: Vec<IntPoint> = contour[i..j].to_vec();
240+
let mut outer: Vec<IntPoint> = contour[..=i].to_vec();
241+
outer.extend_from_slice(&contour[j + 1..]);
242+
243+
let mut result = Vec::new();
244+
if inner.len() >= 3 {
245+
result.extend(split_all_pinch_points(inner, point_buf));
246+
}
247+
if outer.len() >= 3 {
248+
result.extend(split_all_pinch_points(outer, point_buf));
249+
}
250+
result
251+
} else if contour.len() >= 3 {
252+
vec![contour]
253+
} else {
254+
Vec::new()
255+
}
256+
}

0 commit comments

Comments
 (0)