use geo_types::Coord;
use crate::TileError;
use crate::tile::{TileId, tile_of};
#[inline]
const fn shift(c: Coord<i32>, d: i32) -> Coord<i32> {
Coord {
x: c.x + d,
y: c.y + d,
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "callers pass |n| <= d, so the quotient is within |m| and fits i64"
)]
fn mul_div_floor(n: i64, m: i64, d: i64) -> i64 {
match n.checked_mul(m) {
Some(p) => p.div_euclid(d),
None => (i128::from(n) * i128::from(m)).div_euclid(i128::from(d)) as i64,
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Located {
pub(crate) owner: TileId,
core_lo: Coord<i32>,
core_hi: Coord<i32>,
inner_lo: Coord<i32>,
inner_hi: Coord<i32>,
}
impl Located {
pub(crate) fn contains_core(&self, c: Coord<i32>) -> bool {
c.x >= self.core_lo.x
&& c.x <= self.core_hi.x
&& c.y >= self.core_lo.y
&& c.y <= self.core_hi.y
}
pub(crate) fn contains_inner(&self, c: Coord<i32>) -> bool {
c.x >= self.inner_lo.x
&& c.x <= self.inner_hi.x
&& c.y >= self.inner_lo.y
&& c.y <= self.inner_hi.y
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Seg {
lo: Coord<i64>,
hi: Coord<i64>,
dx: i64,
dy: i64,
}
impl Seg {
pub(crate) fn new(lo: Coord<i64>, hi: Coord<i64>) -> Self {
Self {
lo,
hi,
dx: hi.x - lo.x,
dy: hi.y - lo.y,
}
}
#[inline]
fn x_at(self, off: i64, up: bool) -> i64 {
match off {
0 => self.lo.x,
_ if off == self.dy => self.hi.x,
_ if up => self.lo.x - mul_div_floor(-off, self.dx, self.dy),
_ => self.lo.x + mul_div_floor(off, self.dx, self.dy),
}
}
#[inline]
fn row_x(self, ty: i64, extent: i64, buffer: i64) -> (i64, i64) {
let base = ty * extent;
let start = (base - buffer).max(self.lo.y) - self.lo.y;
let end = (base + extent - 1 + buffer).min(self.hi.y) - self.lo.y;
if self.dy == 0 {
(self.lo.x.min(self.hi.x), self.lo.x.max(self.hi.x))
} else if self.dx >= 0 {
(self.x_at(start, true), self.x_at(end, false))
} else {
(self.x_at(end, true), self.x_at(start, false))
}
}
#[inline]
pub(crate) fn row_span(self, ty: i64, extent: i64, buffer: i64) -> (i64, i64) {
let (x_min, x_max) = self.row_x(ty, extent, buffer);
(
(x_min - buffer).div_euclid(extent),
(x_max + buffer).div_euclid(extent),
)
}
fn next_row_after_gap(self, ty: i64, extent: i64) -> i64 {
if self.dx == 0 {
return ty + 1; }
let (x_min, x_max) = self.row_x(ty, extent, 0);
let n = if self.dx > 0 {
x_max + 1 - self.lo.x
} else {
self.lo.x - (x_min - 1)
};
let off = -mul_div_floor(-n, self.dy, self.dx.abs());
((self.lo.y + off).div_euclid(extent)).max(ty + 1)
}
}
#[derive(Clone, Debug)]
pub(crate) struct SegmentTiles {
seg: Seg,
extent: i64,
buffer: i64,
ty: i64,
ty_end: i64,
row: i64,
tx: i64,
tx_end: i64,
}
impl SegmentTiles {
pub(crate) fn new(grid: Grid, a: Coord<i32>, c: Coord<i32>) -> Self {
let (extent, buffer) = (i64::from(grid.extent), i64::from(grid.buffer));
let wide = |p: Coord<i32>| Coord {
x: i64::from(p.x),
y: i64::from(p.y),
};
let (lo, hi) = if a.y <= c.y {
(wide(a), wide(c))
} else {
(wide(c), wide(a))
};
Self {
seg: Seg::new(lo, hi),
extent,
buffer,
ty: (lo.y - buffer).div_euclid(extent),
ty_end: (hi.y + buffer).div_euclid(extent),
row: 0,
tx: 1,
tx_end: 0,
}
}
}
impl Iterator for SegmentTiles {
type Item = TileId;
#[expect(
clippy::cast_possible_truncation,
reason = "tile ids were proved to fit i32 by Grid::check_tiles"
)]
fn next(&mut self) -> Option<TileId> {
loop {
if self.tx <= self.tx_end {
let tile = TileId::new(self.tx as i32, self.row as i32);
self.tx += 1;
return Some(tile);
}
if self.ty > self.ty_end {
return None;
}
let ty = self.ty;
let (first, last) = self.seg.row_span(ty, self.extent, self.buffer);
if first <= last {
(self.row, self.tx, self.tx_end) = (ty, first, last);
self.ty = ty + 1;
} else {
self.ty = self.seg.next_row_after_gap(ty, self.extent);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Grid {
extent: i32,
buffer: i32,
}
impl Grid {
pub(crate) const fn new(extent: u32, buffer: u16) -> Result<Self, TileError> {
if extent == 0 || extent as u64 + buffer as u64 > i32::MAX as u64 {
return Err(TileError::InvalidExtent);
}
if 2 * (buffer as u32) >= extent {
return Err(TileError::BufferTooLarge);
}
Ok(Self {
extent: extent.cast_signed(),
buffer: buffer as i32,
})
}
pub(crate) fn extent(self) -> u32 {
self.extent.cast_unsigned()
}
pub(crate) const fn extent_i32(self) -> i32 {
self.extent
}
pub(crate) const fn buffer_i32(self) -> i32 {
self.buffer
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "buffer is always in 0..=u16::MAX (it was built from a u16)"
)]
pub(crate) fn buffer(self) -> u16 {
self.buffer as u16
}
pub(crate) const fn box_of(self, loc: &Located) -> (Coord<i32>, Coord<i32>) {
(
shift(loc.core_lo, -self.buffer),
shift(loc.core_hi, self.buffer),
)
}
pub(crate) const fn origin_of(self, min: Coord<i32>) -> Coord<i32> {
shift(min, self.buffer)
}
pub(crate) fn tile_buffered_bounds(
self,
tile: TileId,
) -> Result<(Coord<i32>, Coord<i32>), TileError> {
let base_x = tile.x.checked_mul(self.extent).ok_or(TileError::Overflow)?;
let base_y = tile.y.checked_mul(self.extent).ok_or(TileError::Overflow)?;
let reach = (self.extent - 1)
.checked_add(self.buffer)
.ok_or(TileError::Overflow)?;
Ok((
Coord {
x: base_x.checked_sub(self.buffer).ok_or(TileError::Overflow)?,
y: base_y.checked_sub(self.buffer).ok_or(TileError::Overflow)?,
},
Coord {
x: base_x.checked_add(reach).ok_or(TileError::Overflow)?,
y: base_y.checked_add(reach).ok_or(TileError::Overflow)?,
},
))
}
pub(crate) fn tile_box(self, tile: TileId) -> (Coord<i32>, Coord<i32>) {
let base = Coord {
x: tile.x * self.extent,
y: tile.y * self.extent,
};
(
shift(base, -self.buffer),
shift(base, self.extent - 1 + self.buffer),
)
}
#[inline]
pub(crate) fn check_tiles(self, lo: Coord<i32>, hi: Coord<i32>) -> Result<(), TileError> {
if self.near_origin(lo, hi) {
return Ok(());
}
self.check_tiles_exact(lo, hi, 0)
}
#[inline]
fn near_origin(self, lo: Coord<i32>, hi: Coord<i32>) -> bool {
let biased = |v: i32| v.cast_unsigned().wrapping_add(1 << 30);
let in_range = biased(lo.x) | biased(lo.y) | biased(hi.x) | biased(hi.y);
self.extent < 1 << 29 && in_range < 1 << 31
}
fn check_tiles_exact(
self,
lo: Coord<i32>,
hi: Coord<i32>,
margin: i64,
) -> Result<(), TileError> {
let (extent, buffer) = (i64::from(self.extent), i64::from(self.buffer));
let (min, max) = (i64::from(i32::MIN), i64::from(i32::MAX));
for (lo, hi) in [(lo.x, hi.x), (lo.y, hi.y)] {
let first = (i64::from(lo) - buffer).div_euclid(extent);
let last = (i64::from(hi) + buffer).div_euclid(extent);
if first * extent - buffer - margin < min
|| last * extent + extent - 1 + buffer + margin > max
{
return Err(TileError::Overflow);
}
}
Ok(())
}
pub(crate) fn check_bounds(self, lo: Coord<i32>, hi: Coord<i32>) -> Result<(), TileError> {
if self.near_origin_and_small(lo, hi) {
return Ok(());
}
self.check_tiles(lo, hi)?;
let (extent, buffer) = (i64::from(self.extent), i64::from(self.buffer));
for (lo, hi) in [(lo.x, hi.x), (lo.y, hi.y)] {
let first = (i64::from(lo) - buffer).div_euclid(extent);
let last = (i64::from(hi) + buffer).div_euclid(extent);
if i64::from(hi) - first * extent > i64::from(i32::MAX)
|| i64::from(lo) - last * extent < i64::from(i32::MIN)
{
return Err(TileError::Overflow);
}
}
Ok(())
}
pub(crate) fn check_polygon_bounds(
self,
lo: Coord<i32>,
hi: Coord<i32>,
) -> Result<(), TileError> {
if self.near_origin_and_small(lo, hi) {
return Ok(());
}
self.check_bounds(lo, hi)?;
self.check_tiles_exact(lo, hi, 1)
}
#[inline]
fn near_origin_and_small(self, lo: Coord<i32>, hi: Coord<i32>) -> bool {
self.near_origin(lo, hi) && hi.x.abs_diff(lo.x) < 1 << 30 && hi.y.abs_diff(lo.y) < 1 << 30
}
pub(crate) fn locate_fit(self, c: Coord<i32>) -> Located {
let owner = tile_of(c, self.extent);
let (min, max) = self.tile_box(owner);
Located {
owner,
core_lo: shift(min, self.buffer),
core_hi: shift(max, -self.buffer),
inner_lo: shift(min, 2 * self.buffer),
inner_hi: shift(max, -2 * self.buffer),
}
}
pub(crate) fn inner_tile(self, lo: Coord<i32>, hi: Coord<i32>) -> Option<(TileId, Coord<i32>)> {
let (extent, buffer) = (i64::from(self.extent), i64::from(self.buffer));
let axis = |lo: i32, hi: i32| {
let (lo, hi) = (i64::from(lo), i64::from(hi));
let base = lo.div_euclid(extent) * extent;
let inside = lo >= base + buffer && hi <= base + extent - 1 - buffer;
let fits = base - buffer >= i64::from(i32::MIN)
&& base + extent - 1 + buffer <= i64::from(i32::MAX);
#[expect(clippy::cast_possible_truncation, reason = "checked to fit just above")]
(inside && fits).then_some(((base / extent) as i32, base as i32))
};
let (tx, x) = axis(lo.x, hi.x)?;
let (ty, y) = axis(lo.y, hi.y)?;
Some((TileId::new(tx, ty), Coord { x, y }))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clip_polyline::segment_intersects;
#[test]
fn mul_div_floor_is_exact_past_i64() {
let reference = |n: i64, m: i64, d: i64| {
i64::try_from((i128::from(n) * i128::from(m)).div_euclid(i128::from(d))).unwrap()
};
for (n, m, d) in [
(4_000_000_000, 4_294_967_295, 4_294_967_296),
(-3_999_999_999, 4_294_967_295, 4_000_000_000),
(12, -7, 5),
] {
assert_eq!(mul_div_floor(n, m, d), reference(n, m, d), "{n}·{m}/{d}");
}
}
fn scanned(grid: Grid, a: Coord<i32>, c: Coord<i32>) -> Result<Vec<TileId>, TileError> {
let (extent, buffer) = (i64::from(grid.extent), i64::from(grid.buffer));
let tile = |v: i32, d: i64| i32::try_from((i64::from(v) + d).div_euclid(extent)).unwrap();
let mut out = Vec::new();
for ty in tile(a.y.min(c.y), -buffer)..=tile(a.y.max(c.y), buffer) {
for tx in tile(a.x.min(c.x), -buffer)..=tile(a.x.max(c.x), buffer) {
let tile = TileId::new(tx, ty);
let (min, max) = grid.tile_buffered_bounds(tile)?;
if segment_intersects(a, c, min, max) {
out.push(tile);
}
}
}
Ok(out)
}
#[test]
fn walk_matches_bounding_box_scan() {
let mut state = 0x9E37_79B9_7F4A_7C15_u64;
let mut below = |n: u32| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
i64::from(u32::try_from(state % u64::from(n)).unwrap())
};
let mut overflows = 0;
for case in 0..20_000 {
let extent: u32 = [1, 2, 3, 5, 7, 10, 25, 4096][case % 8];
let buffer = u16::try_from(below(extent.div_ceil(2))).unwrap();
let grid = Grid::new(extent, buffer).unwrap();
let span = extent * 40;
let origin = match case % 5 {
0 => i64::from(i32::MIN),
1 => i64::from(i32::MAX) - i64::from(span),
_ => -i64::from(span / 2),
};
let mut coord = || {
let mut v = || i32::try_from(origin + below(span)).unwrap();
Coord { x: v(), y: v() }
};
let (a, mut c) = (coord(), coord());
match below(4) {
0 => c.y = a.y,
1 => c.x = a.x,
2 => c.x = i32::try_from(i64::from(a.x) + below(3) - 1).unwrap_or(a.x),
_ => {}
}
let (lo, hi) = (
Coord {
x: a.x.min(c.x),
y: a.y.min(c.y),
},
Coord {
x: a.x.max(c.x),
y: a.y.max(c.y),
},
);
let reference = scanned(grid, a, c);
let rejected = grid.check_tiles(lo, hi).is_err();
assert_eq!(
rejected,
reference.is_err(),
"{extent}/{buffer} {a:?}-{c:?}"
);
if rejected {
overflows += 1;
continue;
}
let reference = reference.expect("accepted bounds hold every tile box");
let walk: Vec<TileId> = SegmentTiles::new(grid, a, c).collect();
assert_eq!(walk, reference, "{extent}/{buffer} {a:?}-{c:?}");
for &tile in &walk {
assert_eq!(
grid.tile_box(tile),
grid.tile_buffered_bounds(tile).unwrap()
);
}
}
assert!(
overflows > 100,
"the i32 limits are exercised ({overflows})"
);
}
}