Files
endorphin-tessera-gui/src/render/viewport/spatial.rs
T

129 lines
4.2 KiB
Rust

//! Bounded screen-space broad phase. Projective world UI uses the precise
//! ray-based narrow phase rather than an unsafe screen rectangle approximation.
use crate::{document::MountGeometry, geometry::Point};
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct Index {
count: usize,
cells: HashMap<(i32, i32), Vec<usize>>,
fallback: Vec<usize>,
}
impl Index {
pub fn build(mounts: &[MountGeometry]) -> Self {
let mut result = Self {
count: mounts.len(),
..Self::default()
};
let mut entries = 0;
for (index, mount) in mounts.iter().enumerate() {
if !mount.interactive
&& !mount.canvas_viewport
&& !mount.scene3d_viewport
&& mount.scroll.is_none()
{
continue;
}
if mount.panel.is_some() {
result.fallback.push(index);
continue;
}
let a = Point::new(mount.x, mount.y);
let b = Point::new(mount.x + mount.width, mount.y + mount.height);
let a = mount.view.map_or(a, |view| view.point(a));
let b = mount.view.map_or(b, |view| view.point(b));
let (mut x0, mut y0, mut x1, mut y1) =
(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
if let Some([x, y, w, h]) = mount.clip {
x0 = x0.max(x);
y0 = y0.max(y);
x1 = x1.min(x + w);
y1 = y1.min(y + h);
}
if x1 < x0 || y1 < y0 {
continue;
}
if ![x0, y0, x1, y1]
.iter()
.all(|value| value.is_finite() && value.abs() < 1e12)
{
result.fallback.push(index);
continue;
}
let (left, top, right, bottom) = (cell(x0), cell(y0), cell(x1), cell(y1));
let cells = (i64::from(right) - i64::from(left) + 1)
.saturating_mul(i64::from(bottom) - i64::from(top) + 1);
if cells > 256 || entries + cells > 65_536 {
result.fallback.push(index);
continue;
}
entries += cells;
for x in left..=right {
for y in top..=bottom {
result.cells.entry((x, y)).or_default().push(index);
}
}
}
result
}
pub fn candidates(&self, point: Point, count: usize) -> Vec<usize> {
if self.count != count {
return (0..count).rev().collect();
}
let mut output = self.fallback.clone();
if let Some(items) = self.cells.get(&(cell(point.x), cell(point.y))) {
output.extend(items);
}
output.sort_unstable_by(|a, b| b.cmp(a));
output.dedup();
output
}
}
fn cell(value: f32) -> i32 {
(value / 128.)
.floor()
.clamp(i32::MIN as f32, i32::MAX as f32) as i32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn screen_grid_limits_candidates_and_keeps_huge_rectangles_hittable() {
let (_, document) = crate::endor_host::ReplSession::load(
"page=Page()\npage.render(Button(\"hit\"))",
"app.main",
1,
930,
)
.unwrap();
let template = document
.frame
.mounts
.iter()
.find(|mount| mount.interactive)
.unwrap();
let mut mounts = Vec::new();
for index in 0..200 {
let mut mount = template.clone();
mount.x = index as f32 * 400.;
mount.y = 0.;
mount.width = 32.;
mount.height = 32.;
mount.view = None;
mount.clip = None;
mounts.push(mount);
}
let index = Index::build(&mounts);
assert_eq!(index.candidates(Point::new(5., 5.), mounts.len()), vec![0]);
mounts[199].x = -1_000_000.;
mounts[199].width = 2_000_000.;
let index = Index::build(&mounts);
assert_eq!(
index.candidates(Point::new(5., 5.), mounts.len()),
vec![199, 0]
);
assert!(mounts[199].contains(Point::new(5., 5.)));
}
}