Summary
VSegment::is_under_segment_order returns Ordering::Equal for two different segments when one of them starts on the other's line. Output collinear simplification creates exactly that situation (a T-junction) when result shapes touch at a point, and the hole binder's scan cannot order the tie. Depending on the scan structure this:
- panics with
index out of bounds … the index is 9223372036854775807 in bind/solver.rs (32+ shapes, KeyExpTree),
- binds a hole to the wrong shape without panicking (32+ shapes,
KeyExpTree),
- links a shape to a hole it is not inside in
overlay_hierarchy (no threshold, KeyExpList).
All inputs below are valid simple triangles/quads with small integer coordinates, default options (ogc off), i32 engine. Reproduces on 8.1.1 and main (1e33a35), i.e. after #80 and #81. We hit (1) on real OSM building footprints (three map tiles in a planet-wide run).
Steps to reproduce
(1) Panic -- Union, NonZero:
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay::{Overlay, ShapeType};
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::i_float::int::point::IntPoint;
let core = [
vec![[4079, 3454], [4090, 3462], [4083, 3471], [4073, 3464]],
vec![[4078, 3464], [4084, 3463], [4081, 3460]],
vec![[4072, 3463], [4080, 3471], [4061, 3471]],
];
let mut contours: Vec<Vec<IntPoint<i32>>> = core
.iter()
.map(|c| c.iter().map(|&[x, y]| IntPoint::new(x, y)).collect())
.collect();
// 30 separate squares: the result reaches 32 shapes, so the binder uses the tree.
for i in 0..30 {
let x = 100 + i * 40;
contours.push(vec![
IntPoint::new(x, 100), IntPoint::new(x + 10, 100),
IntPoint::new(x + 10, 110), IntPoint::new(x, 110),
]);
}
let mut overlay: Overlay<i32> = Overlay::new(64);
for c in &contours {
overlay.add_contour(c, ShapeType::Subject);
}
let _ = overlay.overlay(OverlayRule::Union, FillRule::NonZero); // panics
With 29 squares (31 result shapes) it succeeds.
(2) Wrong hole owner -- Union, NonZero, these 7 triangles plus 31 separate squares far away:
[[-4,3],[-7,6],[-4,6]], [[-4,2],[-1,5],[-4,5]], [[-3,2],[-10,5],[-6,5]], [[-3,0],[-4,1],[-1,0]],
[[-3,2],[-3,5],[-2,5]], [[-4,2],[-4,4],[0,4]], [[-2,1],[-5,1],[-2,4]]
The hole [(-4,3),(-4,4),(-3,2)] is attached to the small triangle [(-1,0),(-4,1),(-3,0)] instead of the 13-vertex shape that contains it.
(3) Wrong hierarchy link -- overlay_hierarchy(Union, NonZero), no extra squares:
[[-3,-1],[-3,-4],[-7,-4]], [[0,-4],[-1,-4],[-1,0]], [[0,-4],[-3,-7],[-3,-4]], [[0,-2],[-1,-3],[-1,-1]],
[[-1,-1],[-2,-2],[-2,-1]], [[-3,-4],[-3,-7],[-6,-4]], [[0,-2],[-3,-5],[-3,-2]]
The separate triangle [(-2,-2),(-2,-1),(-1,-1)] gets a link to the hole [(-1,-3),(-2,-4),(-1,-4)]; it should have no parent.
Expected
No panic; each hole belongs to the shape that contains it; independent shapes have no hierarchy link.
Actual
thread panicked at i_overlay-8.1.1/src/bind/solver.rs:186:13:
index out of bounds: the len is 1 but the index is 9223372036854775807
for (1); wrong results without warning for (2) and (3).
Environment
- iOverlay: 8.1.1 (also
main at 1e33a35)
- Rust: 1.96.1
- OS: macOS 26.6.2 (Apple Silicon)
Additional context
When the hole's anchor (4078,3464)->(4081,3460) is looked up, the scan holds two live edges:
- A =
(4072,3463)->(4080,3471) above the anchor
- B =
(4073,3464)->(4079,3454) below the anchor (the correct parent)
The two result shapes touch at (4073,3464) and A's contour dropped that vertex as collinear, so B starts on A's line. is_under_segment_order takes the self.a < other.a branch and returns clock_order(A.a, B.a, A.b) = Equal. KeyExpList inserts the equal key before A, so its binary search still finds B; KeyExpTree sends it right (B = A.right), so first_less goes left from A and returns ContourIndex::EMPTY, and resolve_required_parent indexes parent_for_child with EMPTY.index(). The graph build sweep is not affected: it splits at intersections, so its segments never meet this tie.
preserve_output_collinear = true avoids it (no T-junctions survive), at the cost of collinear vertices in the output.
PR #70 reported the same panic and suspected Equal in VSegment::Ord, without a valid repro.
Proposal: break the tie on the later segment's far end, the same argument pattern the shared-start (Equal) branch already uses:
Ordering::Less => Triangle::clock_order(self.a, other.a, self.b)
.then_with(|| Triangle::clock_order(self.a, other.b, self.b)),
Ordering::Equal => Triangle::clock_order(self.a, other.b, self.b),
Ordering::Greater => Triangle::clock_order(other.a, other.b, self.a)
.then_with(|| Triangle::clock_order(other.a, other.b, self.b)),
The second orientation test only runs on a tie, so the common path is unchanged (a 300k-polygon union benchmarked within noise). Both branches are needed for antisymmetry: fixing only the Greater branch still gives wrong results. Checked against a brute-force "first segment below the anchor" oracle inside the binder over 70k random touching-polygon cases (overlay and overlay_hierarchy, list and tree paths): 3,631 wrong bindings without the change, 0 with it.
I have the change with tests (the three repros plus a comparator unit test) ready and can open a PR if helpful! But wanted to check with you first.
Summary
VSegment::is_under_segment_orderreturnsOrdering::Equalfor two different segments when one of them starts on the other's line. Output collinear simplification creates exactly that situation (a T-junction) when result shapes touch at a point, and the hole binder's scan cannot order the tie. Depending on the scan structure this:index out of bounds … the index is 9223372036854775807inbind/solver.rs(32+ shapes,KeyExpTree),KeyExpTree),overlay_hierarchy(no threshold,KeyExpList).All inputs below are valid simple triangles/quads with small integer coordinates, default options (
ogcoff), i32 engine. Reproduces on 8.1.1 andmain(1e33a35), i.e. after #80 and #81. We hit (1) on real OSM building footprints (three map tiles in a planet-wide run).Steps to reproduce
(1) Panic --
Union,NonZero:With 29 squares (31 result shapes) it succeeds.
(2) Wrong hole owner --
Union,NonZero, these 7 triangles plus 31 separate squares far away:The hole
[(-4,3),(-4,4),(-3,2)]is attached to the small triangle[(-1,0),(-4,1),(-3,0)]instead of the 13-vertex shape that contains it.(3) Wrong hierarchy link --
overlay_hierarchy(Union, NonZero), no extra squares:The separate triangle
[(-2,-2),(-2,-1),(-1,-1)]gets a link to the hole[(-1,-3),(-2,-4),(-1,-4)]; it should have no parent.Expected
No panic; each hole belongs to the shape that contains it; independent shapes have no hierarchy link.
Actual
for (1); wrong results without warning for (2) and (3).
Environment
mainat 1e33a35)Additional context
When the hole's anchor
(4078,3464)->(4081,3460)is looked up, the scan holds two live edges:(4072,3463)->(4080,3471)above the anchor(4073,3464)->(4079,3454)below the anchor (the correct parent)The two result shapes touch at
(4073,3464)and A's contour dropped that vertex as collinear, so B starts on A's line.is_under_segment_ordertakes theself.a < other.abranch and returnsclock_order(A.a, B.a, A.b)=Equal.KeyExpListinserts the equal key before A, so its binary search still finds B;KeyExpTreesends it right (B = A.right), sofirst_lessgoes left from A and returnsContourIndex::EMPTY, andresolve_required_parentindexesparent_for_childwithEMPTY.index(). The graph build sweep is not affected: it splits at intersections, so its segments never meet this tie.preserve_output_collinear = trueavoids it (no T-junctions survive), at the cost of collinear vertices in the output.PR #70 reported the same panic and suspected
EqualinVSegment::Ord, without a valid repro.Proposal: break the tie on the later segment's far end, the same argument pattern the shared-start (
Equal) branch already uses:The second orientation test only runs on a tie, so the common path is unchanged (a 300k-polygon union benchmarked within noise). Both branches are needed for antisymmetry: fixing only the
Greaterbranch still gives wrong results. Checked against a brute-force "first segment below the anchor" oracle inside the binder over 70k random touching-polygon cases (overlay and overlay_hierarchy, list and tree paths): 3,631 wrong bindings without the change, 0 with it.I have the change with tests (the three repros plus a comparator unit test) ready and can open a PR if helpful! But wanted to check with you first.