diff --git a/extractrs-core/src/bbox.rs b/extractrs-core/src/bbox.rs index ad8c507..d65ea9c 100644 --- a/extractrs-core/src/bbox.rs +++ b/extractrs-core/src/bbox.rs @@ -154,6 +154,16 @@ impl BBox { /// Uses quadrant-based projection with clamping for numerical robustness. /// /// Port of exactextract's `Box::crossing()` (box.cpp:43-118). + /// + /// **Degenerate fallback:** the original exactextract implementation + /// throws "Never get here" when a vertical/horizontal segment lies + /// entirely outside the box (i.e., the line never intersects this box). + /// This can be triggered by low-quality polygon data with collinear + /// vertices that happen to fall outside the current cell — see + /// isciences/exactextract#126. Rather than panic, we return the box + /// side closest to the segment so the traversal can make progress. + /// Callers should also avoid invoking `crossing()` in this state when + /// possible; see `Cell::take()` for the upstream guard. pub fn crossing(&self, c1: &Coord, c2: &Coord) -> Crossing { // Vertical line (x constant) if c1.x == c2.x { @@ -162,7 +172,14 @@ impl BBox { } else if c2.y <= self.ymin { return Crossing::new(Side::Bottom, c1.x, self.ymin); } else { - unreachable!("vertical line doesn't exit box"); + // Degenerate: vertical line entirely outside the box on x. + // Return the closest side so the traversal can advance. + let y = clamp(c2.y, self.ymin, self.ymax); + if c1.x <= self.xmin { + return Crossing::new(Side::Left, self.xmin, y); + } else { + return Crossing::new(Side::Right, self.xmax, y); + } } } @@ -173,7 +190,13 @@ impl BBox { } else if c2.x <= self.xmin { return Crossing::new(Side::Left, self.xmin, c1.y); } else { - unreachable!("horizontal line doesn't exit box"); + // Degenerate: horizontal line entirely outside the box on y. + let x = clamp(c2.x, self.xmin, self.xmax); + if c1.y <= self.ymin { + return Crossing::new(Side::Bottom, x, self.ymin); + } else { + return Crossing::new(Side::Top, x, self.ymax); + } } } @@ -290,6 +313,40 @@ mod tests { assert!((c.coord.y - 1.0).abs() < 1e-10); } + #[test] + fn test_crossing_vertical_degenerate_left_of_box() { + // Vertical segment whose x is strictly left of the box. + // Pre-fix this would panic with "vertical line doesn't exit box". + let b = unit_box(); + let c = b.crossing(&Coord::new(-2.0, 0.5), &Coord::new(-2.0, 0.5)); + assert_eq!(c.side, Side::Left); + assert_eq!(c.coord.x, 0.0); + } + + #[test] + fn test_crossing_vertical_degenerate_right_of_box() { + let b = unit_box(); + let c = b.crossing(&Coord::new(5.0, 0.5), &Coord::new(5.0, 0.5)); + assert_eq!(c.side, Side::Right); + assert_eq!(c.coord.x, 1.0); + } + + #[test] + fn test_crossing_horizontal_degenerate_above_box() { + let b = unit_box(); + let c = b.crossing(&Coord::new(0.5, 5.0), &Coord::new(0.5, 5.0)); + assert_eq!(c.side, Side::Top); + assert_eq!(c.coord.y, 1.0); + } + + #[test] + fn test_crossing_horizontal_degenerate_below_box() { + let b = unit_box(); + let c = b.crossing(&Coord::new(0.5, -2.0), &Coord::new(0.5, -2.0)); + assert_eq!(c.side, Side::Bottom); + assert_eq!(c.coord.y, 0.0); + } + #[test] fn test_crossing_diagonal_steep_up_right() { let b = unit_box(); diff --git a/extractrs-core/src/cell.rs b/extractrs-core/src/cell.rs index 44518eb..214845b 100644 --- a/extractrs-core/src/cell.rs +++ b/extractrs-core/src/cell.rs @@ -92,7 +92,19 @@ impl Cell { // Coordinate exits cell. // Use prev_original for crossing computation to avoid robustness issues // from interpolated coordinates (exactextract cell.cpp:126-129). - let crossing = if let Some(prev) = prev_original { + // + // Skip prev_original if it would form a degenerate vertical/horizontal + // segment lying entirely outside the cell — last_coordinate is always + // on the boundary or interior so it's a safe fallback. See + // isciences/exactextract#126 for the data conditions that trigger this. + let prev_safe = prev_original.filter(|prev| { + let vertical_outside = prev.x == c.x + && (prev.x < self.bbox.xmin || prev.x > self.bbox.xmax); + let horizontal_outside = prev.y == c.y + && (prev.y < self.bbox.ymin || prev.y > self.bbox.ymax); + !vertical_outside && !horizontal_outside + }); + let crossing = if let Some(prev) = prev_safe { self.bbox.crossing(prev, c) } else { let last = *self.traversals[idx].last_coordinate();