use alloc::{rc::Rc, vec::Vec};
use core::{
array::from_fn,
cmp::Ordering,
fmt::{self, Debug, Formatter},
marker::PhantomData,
ops::{Add, AddAssign, Deref, Div, Mul, Neg, Sub, SubAssign},
primitive::{f64, u32, usize},
result::Result,
};
use crate::Error;
#[repr(transparent)]
pub struct Value<Tag>(f64, PhantomData<Tag>);
impl<Tag> Clone for Value<Tag> {
fn clone(&self) -> Self {
*self
}
}
impl<Tag> Copy for Value<Tag> {}
impl<Tag> Value<Tag> {
pub fn new(v: f64) -> Self {
Self(v, PhantomData)
}
pub fn get(self) -> f64 {
self.0
}
}
impl<Tag> Debug for Value<Tag> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_tuple("Value").field(&self.0).finish()
}
}
impl<Tag> PartialEq for Value<Tag> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<Tag> PartialOrd for Value<Tag> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.0.partial_cmp(&other.0)
}
}
impl<Tag> Default for Value<Tag> {
fn default() -> Self {
Self::new(0.0)
}
}
impl<Tag> Neg for Value<Tag> {
type Output = Value<Tag>;
fn neg(self) -> Self::Output {
Value::new(-self.0)
}
}
impl<Tag> SubAssign for Value<Tag> {
fn sub_assign(&mut self, rhs: Self) {
self.0 -= rhs.0;
}
}
impl<Tag> Mul<Value<Tag>> for f64 {
type Output = Value<Tag>;
fn mul(self, rhs: Value<Tag>) -> Self::Output {
Value::new(self * rhs.0)
}
}
impl<Tag> Div<f64> for Value<Tag> {
type Output = Value<Tag>;
fn div(self, rhs: f64) -> Self::Output {
Value::new(self.0 / rhs)
}
}
impl<Tag> From<f64> for Value<Tag> {
fn from(v: f64) -> Self {
Self::new(v)
}
}
impl<Tag> Add for Value<Tag> {
type Output = Value<Tag>;
fn add(self, rhs: Self) -> Self::Output {
Value::new(self.0 + rhs.0)
}
}
impl<Tag> AddAssign for Value<Tag> {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl<Tag> Sub for Value<Tag> {
type Output = Value<Tag>;
fn sub(self, rhs: Self) -> Self::Output {
Value::new(self.0 - rhs.0)
}
}
impl<Tag> Mul<f64> for Value<Tag> {
type Output = Value<Tag>;
fn mul(self, rhs: f64) -> Self::Output {
Value::new(self.0 * rhs)
}
}
impl<Tag> Div for Value<Tag> {
type Output = f64;
fn div(self, rhs: Self) -> f64 {
self.0 / rhs.0
}
}
pub struct PxTag;
pub struct UnitTag;
pub struct ParameterTag;
pub type Px = Value<PxTag>;
pub type Unit = Value<UnitTag>;
pub type Parameter = Value<ParameterTag>;
pub type Point<const D: usize> = [Unit; D];
#[derive(Clone, Copy)]
#[repr(C)]
pub struct BBox<const D: usize> {
pub(crate) base: Point<D>,
pub(crate) offset: Point<D>,
}
impl<const D: usize> BBox<D> {
pub fn new(base: Point<D>, offset: Point<D>) -> Self {
let mut base = base;
let mut offset = offset;
for d in 0..D {
if offset[d].get() < 0.0 {
base[d] += offset[d];
offset[d] = -offset[d];
}
}
Self { base, offset }
}
pub fn base(&self) -> Point<D> {
self.base
}
pub fn offset(&self) -> Point<D> {
self.offset
}
pub fn snap_floor(&mut self, extend: Option<[Unit; D]>) -> &mut Self {
for (d, u) in self.base.iter_mut().enumerate() {
let v = if let Some(ext) = extend { *u + ext[d] } else { *u };
*u = Unit::new(libm::floor(v.get()));
}
for u in self.offset.iter_mut() {
*u = Unit::new(libm::floor(u.get()));
}
self
}
pub fn has_size(&self) -> bool {
self.offset.iter().all(|u| u.get() != 0.0)
}
}
pub trait StepFn {
fn step(&self, i: u32) -> Result<Px, Error>;
}
impl<P> StepFn for P
where
P: Deref,
P::Target: Fn(u32) -> Result<Px, Error>,
{
fn step(&self, i: u32) -> Result<Px, Error> {
(**self)(i)
}
}
pub type DefaultSteps = Rc<dyn Fn(u32) -> Result<Px, Error>>;
pub enum IncrementFunction<P = DefaultSteps> {
ForwardDifference(P),
VectorList(Vec<Px>),
Scale(f64),
}
impl<P: StepFn> IncrementFunction<P> {
pub fn accumulate(self) -> Result<Accumulator<P>, Error> {
match self {
Self::Scale(s) => {
if s.is_nan() {
return Err(Error::InvalidInput);
}
Ok(Accumulator::Scale(s))
}
Self::VectorList(pxs) => {
if pxs.is_empty() {
return Err(Error::InvalidDefinition);
}
Ok(Accumulator::VectorList(pxs))
}
Self::ForwardDifference(f) => Ok(Accumulator::ForwardDifference(f)),
}
}
}
fn forward_difference_forward<P: StepFn>(f: &P, x: f64) -> Result<Px, Error> {
let n = libm::floor(x) as u32;
let frac = x - n as f64;
let mut acc = Px::new(0.0);
for k in 0..n {
acc += f.step(k)?;
}
if frac != 0.0 {
acc += f.step(n)? * frac;
}
Ok(acc)
}
fn forward_difference_inverse<P: StepFn>(f: &P, target: Px) -> Result<Unit, Error> {
let target = target.get();
if !target.is_finite() {
return Err(Error::InvalidInput);
}
if target == 0.0 {
return Ok(Unit::new(0.0));
}
if target < 0.0 {
let step = f.step(0)?.get();
if step == 0.0 {
return Err(Error::OutOfIndex(0));
}
return Ok(Unit::new(target / step));
}
let mut accumulated = 0.0;
for k in 0..=u32::MAX {
let step = f.step(k)?.get();
let next = accumulated + step;
if next >= target {
let frac = if next == target {
1.0
} else if step == 0.0 {
0.0
} else {
((target - accumulated) / step).min(1.0)
};
return Ok(Unit::new(k as f64 + frac));
}
accumulated = next;
}
Err(Error::OutOfIndex(u32::MAX))
}
pub enum Accumulator<P = DefaultSteps> {
Scale(f64),
VectorList(Vec<Px>),
ForwardDifference(P),
}
impl<P: StepFn> Accumulator<P> {
pub fn forward(&self, x: f64) -> Result<Px, Error> {
if x.is_nan() {
return Err(Error::InvalidInput);
}
match self {
Self::Scale(s) => Ok(Px::new(s * x)),
Self::VectorList(pxs) => {
let last = (pxs.len() - 1) as u32;
if x < 0.0 {
return Err(Error::OutOfIndex(0));
}
let n = libm::floor(x) as usize;
let frac = x - n as f64;
let lo = *pxs.get(n).ok_or(Error::OutOfIndex(last))?;
if frac == 0.0 {
return Ok(lo);
}
let hi = *pxs.get(n + 1).ok_or(Error::OutOfIndex(last))?;
Ok(lo + (hi - lo) * frac)
}
Self::ForwardDifference(f) => forward_difference_forward(f, x),
}
}
pub fn inverse(&self, target: Px) -> Result<Unit, Error> {
if target.get().is_nan() {
return Err(Error::InvalidInput);
}
match self {
Self::Scale(s) => {
let quotient = target.get() / s;
let nearest = libm::round(quotient);
Ok(Unit::new(if s * nearest == target.get() { nearest } else { quotient }))
}
Self::VectorList(pxs) => vector_list_inverse(pxs, target),
Self::ForwardDifference(f) => forward_difference_inverse(f, target),
}
}
}
fn vector_list_inverse(pxs: &[Px], target: Px) -> Result<Unit, Error> {
let last = (pxs.len() - 1) as u32;
let t = target.get();
if t < pxs[0].get() {
return Err(Error::OutOfIndex(0));
}
if t > pxs[pxs.len() - 1].get() {
return Err(Error::OutOfIndex(last));
}
let n = pxs.partition_point(|p| p.get() < t).saturating_sub(1);
let lo = pxs[n].get();
let hi = pxs.get(n + 1).map(|p| p.get()).unwrap_or(lo);
if hi == lo {
return Ok(Unit::new(n as f64));
}
let frac = (t - lo) / (hi - lo);
Ok(Unit::new(n as f64 + frac))
}
pub struct RectGrid<const D: usize, P = DefaultSteps> {
pub origin: [Px; D],
accumulator: [Accumulator<P>; D],
}
impl<const D: usize, P: StepFn> RectGrid<D, P> {
pub fn new(origin: [Px; D], definitions: [IncrementFunction<P>; D]) -> Result<Self, Error> {
let accumulator: Vec<_> =
definitions.into_iter().map(|d| d.accumulate()).collect::<Result<_, _>>()?;
let accumulator = match accumulator.try_into() {
Ok(a) => a,
Err(_) => unreachable!("definitions and accumulator share length D"),
};
Ok(Self { origin, accumulator })
}
pub fn set_definition(
&mut self,
definition: IncrementFunction<P>,
d: usize,
) -> Result<(), Error> {
self.accumulator[d] = definition.accumulate()?;
Ok(())
}
pub fn point_to_unit(&self, point: [Px; D]) -> [Result<Unit, Error>; D] {
from_fn(|i| self.px_to_unit_axis(i, point[i] - self.origin[i]))
}
fn px_to_unit_axis(&self, i: usize, target: Px) -> Result<Unit, Error> {
self.accumulator[i].inverse(target)
}
pub fn unit_to_px(&self, d: usize, unit: &Unit) -> Result<Px, Error> {
self.accumulator[d].forward(unit.get())
}
pub fn point_as_px(&self, points: &[Point<D>]) -> Vec<[Result<Px, Error>; D]> {
points.iter().map(|pt| from_fn(|d| self.unit_to_px(d, &pt[d]))).collect()
}
fn unit_to_px_clipped(&self, d: usize, unit: &Unit) -> Result<Px, Error> {
match self.unit_to_px(d, unit) {
Err(Error::OutOfIndex(last)) => self.accumulator[d].forward(last as f64),
other => other,
}
}
fn contains(
&self,
point: [Px; D],
bx: &BBox<D>,
extend: Option<([Unit; D], [Unit; D])>,
) -> Option<(bool, [Px; D], [Px; D])> {
let mut base_px = [Px::new(0.0); D];
let mut offset_px = [Px::new(0.0); D];
let mut hit = true;
for d in 0..D {
base_px[d] = self.unit_to_px(d, &bx.base[d]).ok()?;
offset_px[d] = self.unit_to_px(d, &(bx.base[d] + bx.offset[d])).ok()?;
let (lo, hi) = match extend {
Some((eb, eo)) => (
self.unit_to_px_clipped(d, &(bx.base[d] + eb[d])).ok()?,
self.unit_to_px_clipped(d, &(bx.base[d] + bx.offset[d] + eo[d])).ok()?,
),
None => (base_px[d], offset_px[d]),
};
let local = point[d] - self.origin[d];
hit &= local.get() >= lo.get() && local.get() <= hi.get();
}
Some((hit, base_px, offset_px))
}
fn parameter_from_px(point: [Px; D], base_px: [Px; D], offset_px: [Px; D]) -> [Parameter; D] {
from_fn(|d| Self::parameter_axis(point[d], base_px[d], offset_px[d]))
}
fn parameter_axis(point: Px, base_px: Px, far_px: Px) -> Parameter {
let width = far_px - base_px;
if width.get() == 0.0 {
Parameter::new(0.0)
} else {
Parameter::new((point - base_px) / width)
}
}
pub fn hit_test(
&self,
point: [Px; D],
boxes: &[BBox<D>],
extend: Option<([Unit; D], [Unit; D])>,
) -> Option<usize> {
boxes.iter().enumerate().rev().find_map(|(i, bx)| {
matches!(self.contains(point, bx, extend), Some((true, _, _))).then_some(i)
})
}
pub fn hit_test_with_parameter(
&self,
point: [Px; D],
boxes: &[BBox<D>],
extend: Option<([Unit; D], [Unit; D])>,
) -> Option<(usize, [Parameter; D])> {
let local: [Px; D] = from_fn(|d| point[d] - self.origin[d]);
boxes.iter().enumerate().rev().find_map(|(i, bx)| {
let (hit, base_px, offset_px) = self.contains(point, bx, extend)?;
hit.then(|| (i, Self::parameter_from_px(local, base_px, offset_px)))
})
}
pub fn hit_tests(
&self,
point: [Px; D],
boxes: &[BBox<D>],
extend: Option<([Unit; D], [Unit; D])>,
) -> Vec<bool> {
boxes
.iter()
.map(|bx| matches!(self.contains(point, bx, extend), Some((true, _, _))))
.collect()
}
pub fn get_parameter(&self, point: [Px; D], bx: BBox<D>) -> [Result<Parameter, Error>; D] {
from_fn(|d| -> Result<Parameter, Error> {
let base = self.unit_to_px(d, &bx.base[d])?;
let far = self.unit_to_px(d, &(bx.base[d] + bx.offset[d]))?;
Ok(Self::parameter_axis(point[d] - self.origin[d], base, far))
})
}
pub fn box_as_px(&self, boxes: &[BBox<D>]) -> Vec<[Result<(Px, Px), Error>; D]> {
boxes
.iter()
.map(|bx| {
from_fn(|d| -> Result<(Px, Px), Error> {
let base = self.unit_to_px(d, &bx.base[d])?;
let far = self.unit_to_px(d, &(bx.base[d] + bx.offset[d]))?;
Ok((base, far - base))
})
})
.collect()
}
pub fn offset(&self, pointer: [Px; D], z: [Px; D]) -> [Px; D] {
from_fn(|d| (pointer[d] - self.origin[d]) - z[d])
}
}
pub fn corner_test<const D: usize, P: StepFn>(
grid: &RectGrid<D, P>,
point: [Px; D],
bx: &BBox<D>,
threshold: f64,
extend: Option<([Unit; D], [Unit; D])>,
) -> (Option<[Parameter; D]>, Option<[Option<bool>; D]>) {
if !bx.has_size() {
return (None, None);
}
let mut parameter = [Parameter::new(0.0); D];
for (axis, result) in grid.get_parameter(point, *bx).into_iter().enumerate() {
match result {
Ok(value) => parameter[axis] = value,
Err(_) => return (None, None),
}
}
let margin: [(f64, f64); D] = from_fn(|d| match extend {
Some((eb, eo)) => {
let offset = bx.offset()[d].get();
if offset == 0.0 { (0.0, 0.0) } else { (eb[d].get() / offset, eo[d].get() / offset) }
}
None => (0.0, 0.0),
});
let inside =
parameter.iter().zip(margin).all(|(r, (lo, hi))| r.get() >= lo && r.get() <= 1.0 + hi);
if !inside {
return (None, None);
}
let corner: [Option<bool>; D] = from_fn(|d| {
let r = parameter[d].get();
if r <= threshold {
Some(true)
} else if r >= 1.0 - threshold {
Some(false)
} else {
None
}
});
let corner = if corner.iter().any(Option::is_some) { Some(corner) } else { None };
(Some(parameter), corner)
}
pub fn drag_resize<const D: usize, P: StepFn>(
grid: &RectGrid<D, P>,
pointer: [Px; D],
bx: &BBox<D>,
corner: [Option<bool>; D],
) -> Result<BBox<D>, Error> {
let unit = grid.point_to_unit(pointer);
let mut resized = *bx;
for d in 0..D {
let Some(base_side) = corner[d] else { continue };
let new_u = Unit::new(libm::floor(unit[d]?.get()));
let base_u = bx.base[d];
let offset_u = bx.offset[d];
if base_side {
let far_edge = base_u + offset_u;
let clamped_u = Unit::new(new_u.get().min((far_edge - Unit::new(1.0)).get()));
resized.base[d] = clamped_u;
resized.offset[d] = Unit::new((far_edge - clamped_u).get().max(1.0));
} else {
let new_offset = (new_u - base_u).get().max(1.0);
resized.offset[d] = Unit::new(new_offset);
}
}
Ok(resized)
}
pub fn drag_translate<const D: usize, P: StepFn>(
grid: &RectGrid<D, P>,
pointer: [Px; D],
drag_offset: [Px; D],
) -> [Px; D] {
grid.offset(pointer, drag_offset)
}
pub fn snap_bbox_to_unit<const D: usize, P: StepFn>(
grid: &RectGrid<D, P>,
pointer: [Px; D],
drag_offset: [Px; D],
bx: &BBox<D>,
extend: Option<[Unit; D]>,
) -> Result<BBox<D>, Error> {
let mut snapped = *bx;
for d in 0..D {
let local = pointer[d] - grid.origin[d] - drag_offset[d];
snapped.base[d] = grid.px_to_unit_axis(d, local)?;
}
snapped.snap_floor(extend);
Ok(snapped)
}
pub fn snap_point_to_unit<const D: usize, P: StepFn>(
grid: &RectGrid<D, P>,
pointer: [Px; D],
drag_offset: [Px; D],
snap: [Unit; D],
) -> Result<BBox<D>, Error> {
let mut base: Point<D> = [Unit::new(0.0); D];
for d in 0..D {
let local = pointer[d] - grid.origin[d] - drag_offset[d];
let u = grid.px_to_unit_axis(d, local)?;
base[d] = Unit::new(libm::floor((u + snap[d]).get()));
}
Ok(BBox { base, offset: from_fn(|_| Unit::new(0.0)) })
}
#[cfg(test)]
mod tests {
use alloc::rc::Rc;
use super::*;
#[test]
fn point_to_unit_scale_roundtrip() {
let grid = RectGrid::<2>::new(
[Px::new(0.0), Px::new(0.0)],
[IncrementFunction::Scale(200.0), IncrementFunction::Scale(64.0)],
)
.unwrap();
let result = grid.point_to_unit([Px::new(450.0), Px::new(10.0)]);
let x = result[0].as_ref().unwrap().get();
let y = result[1].as_ref().unwrap().get();
assert!((x - 2.25).abs() < 1e-6, "x = {}", x);
assert!((y - 0.15625).abs() < 1e-6, "y = {}", y);
}
#[test]
fn point_to_unit_vector_list_roundtrip() {
let grid = RectGrid::<1>::new(
[Px::new(0.0)],
[IncrementFunction::VectorList(alloc::vec![
Px::new(0.0),
Px::new(10.0),
Px::new(30.0),
Px::new(60.0)
])],
)
.unwrap();
let result = grid.point_to_unit([Px::new(45.0)]);
let x = result[0].as_ref().unwrap().get();
assert!((x - 2.5).abs() < 1e-6, "x = {}", x);
}
#[test]
fn point_to_unit_vector_list_out_of_range() {
let grid = RectGrid::<1>::new(
[Px::new(0.0)],
[IncrementFunction::VectorList(alloc::vec![
Px::new(0.0),
Px::new(10.0),
Px::new(30.0),
Px::new(60.0)
])],
)
.unwrap();
let result = grid.point_to_unit([Px::new(100.0)]);
assert!(matches!(result[0], Err(Error::OutOfIndex(3))));
}
#[test]
fn point_to_unit_applies_origin_offset() {
let grid = RectGrid::<2>::new(
[Px::new(10.0), Px::new(20.0)],
[IncrementFunction::Scale(200.0), IncrementFunction::Scale(64.0)],
)
.unwrap();
let result = grid.point_to_unit([Px::new(230.0), Px::new(50.0)]);
let x = result[0].as_ref().unwrap().get();
let y = result[1].as_ref().unwrap().get();
assert!((x - 1.1).abs() < 1e-6, "x = {}", x);
assert!((y - 0.46875).abs() < 1e-6, "y = {}", y);
}
#[test]
fn accumulate_forward_difference() {
let f =
IncrementFunction::ForwardDifference(Rc::new(|i| Ok(Px::new((i + 1) as f64 * 10.0))));
let acc = f.accumulate().unwrap();
assert_eq!(acc.forward(0.0).unwrap().get(), 0.0);
assert!((acc.forward(2.5).unwrap().get() - 45.0).abs() < 1e-9);
}
#[test]
fn accumulator_scale_inverse_is_exact() {
let acc = IncrementFunction::<DefaultSteps>::Scale(200.0).accumulate().unwrap();
assert_eq!(acc.inverse(Px::new(450.0)).unwrap().get(), 2.25);
}
#[test]
fn accumulator_vector_list_inverse_boundaries() {
let acc = IncrementFunction::<DefaultSteps>::VectorList(alloc::vec![
Px::new(0.0),
Px::new(10.0),
Px::new(30.0)
])
.accumulate()
.unwrap();
assert_eq!(acc.inverse(Px::new(0.0)).unwrap().get(), 0.0);
assert_eq!(acc.inverse(Px::new(30.0)).unwrap().get(), 2.0);
assert!((acc.inverse(Px::new(20.0)).unwrap().get() - 1.5).abs() < 1e-9);
assert!(matches!(acc.inverse(Px::new(-1.0)), Err(Error::OutOfIndex(0))));
assert!(matches!(acc.inverse(Px::new(31.0)), Err(Error::OutOfIndex(2))));
}
#[test]
fn bbox_new_normalizes_negative_offset() {
let bx = BBox::new([Unit::new(0.0)], [Unit::new(-1.0)]);
assert_eq!(bx.base()[0].get(), -1.0);
assert_eq!(bx.offset()[0].get(), 1.0);
let bx = BBox::new([Unit::new(0.0)], [Unit::new(0.0)]);
assert_eq!(bx.base()[0].get(), 0.0);
assert_eq!(bx.offset()[0].get(), 0.0);
}
#[test]
fn snap_floor_no_extend() {
let mut bx = BBox { base: [Unit::new(2.7)], offset: [Unit::new(1.9)] };
bx.snap_floor(None);
assert_eq!(bx.base[0].get(), 2.0);
assert_eq!(bx.offset[0].get(), 1.0);
}
#[test]
fn box_as_px_out_of_index() {
let grid = RectGrid::<1>::new(
[Px::new(0.0)],
[IncrementFunction::VectorList(alloc::vec![Px::new(0.0), Px::new(10.0)])],
)
.unwrap();
let boxes = alloc::vec![BBox { base: [Unit::new(0.0)], offset: [Unit::new(5.0)] }];
assert!(matches!(grid.box_as_px(&boxes)[0][0], Err(Error::OutOfIndex(1))));
}
#[test]
fn hit_test_with_parameter_no_hit() {
let grid = RectGrid::<2>::new(
[Px::new(0.0), Px::new(0.0)],
[IncrementFunction::Scale(200.0), IncrementFunction::Scale(64.0)],
)
.unwrap();
let boxes = alloc::vec![BBox {
base: [Unit::new(0.0), Unit::new(0.0)],
offset: [Unit::new(1.0), Unit::new(1.0)],
},];
assert!(
grid.hit_test_with_parameter([Px::new(500.0), Px::new(10.0)], &boxes, None).is_none()
);
}
#[test]
fn hit_tests_partial_and_no_hit() {
let grid = RectGrid::<2>::new(
[Px::new(0.0), Px::new(0.0)],
[IncrementFunction::Scale(200.0), IncrementFunction::Scale(64.0)],
)
.unwrap();
let boxes = alloc::vec![
BBox {
base: [Unit::new(0.0), Unit::new(0.0)],
offset: [Unit::new(1.0), Unit::new(1.0)],
},
BBox {
base: [Unit::new(2.0), Unit::new(0.0)],
offset: [Unit::new(1.0), Unit::new(1.0)],
},
];
assert_eq!(
grid.hit_tests([Px::new(50.0), Px::new(10.0)], &boxes, None),
alloc::vec![true, false]
);
assert_eq!(
grid.hit_tests([Px::new(-1.0), Px::new(-1.0)], &boxes, None),
alloc::vec![false, false]
);
}
#[test]
fn drag_resize_offset_side() {
let grid = RectGrid::<2>::new(
[Px::new(0.0), Px::new(0.0)],
[IncrementFunction::Scale(200.0), IncrementFunction::Scale(64.0)],
)
.unwrap();
let bx = BBox {
base: [Unit::new(2.0), Unit::new(0.0)],
offset: [Unit::new(1.0), Unit::new(3.0)],
};
let resized =
drag_resize(&grid, [Px::new(900.0), Px::new(0.0)], &bx, [Some(false), None]).unwrap();
assert_eq!(resized.base[0].get(), 2.0);
assert_eq!(resized.offset[0].get(), 2.0);
}
#[test]
fn drag_resize_minimum_clamp() {
let grid = RectGrid::<2>::new(
[Px::new(0.0), Px::new(0.0)],
[IncrementFunction::Scale(200.0), IncrementFunction::Scale(64.0)],
)
.unwrap();
let bx = BBox {
base: [Unit::new(2.0), Unit::new(0.0)],
offset: [Unit::new(2.0), Unit::new(3.0)],
};
let resized =
drag_resize(&grid, [Px::new(1100.0), Px::new(0.0)], &bx, [Some(true), None]).unwrap();
assert_eq!(resized.base[0].get(), 3.0);
assert_eq!(resized.offset[0].get(), 1.0);
}
type Steps = Rc<dyn Fn(u32) -> Result<Px, Error>>;
fn wavy(k: u32) -> Result<Px, Error> {
Ok(Px::new(1.0 + libm::fabs(libm::sin(k as f64 * 0.37)) * 0.01 + 1e-3 * (k % 7) as f64))
}
fn growing(k: u32) -> Result<Px, Error> {
Ok(Px::new(libm::pow(1.0001, k as f64)))
}
fn irregular(k: u32) -> Result<Px, Error> {
let mut z = (k as u64).wrapping_add(0x9E3779B97F4A7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^= z >> 31;
Ok(Px::new(0.5 + (z >> 11) as f64 / (1u64 << 53) as f64))
}
fn accumulator(steps: Steps) -> Accumulator {
IncrementFunction::ForwardDifference(steps).accumulate().unwrap()
}
#[test]
fn forward_difference_inverse_roundtrip_is_exact() {
let datasets: [(&str, Steps, &[f64]); 6] = [
(
"wavy",
Rc::new(wavy),
&[0.25, 0.999, 1.0, 3.5, 99.75, 1000.125, 12345.0137, 50_000.5],
),
("constant 1e-9", Rc::new(|_| Ok(Px::new(1e-9))), &[0.25, 3.5, 1000.125, 20_000.75]),
("constant 1e9", Rc::new(|_| Ok(Px::new(1e9))), &[0.25, 3.5, 1000.125, 20_000.75]),
("growing 1.0001^k", Rc::new(growing), &[0.5, 17.25, 1000.5, 4000.75]),
("irregular", Rc::new(irregular), &[0.1, 1.9, 50.5, 1234.5678, 30_000.25]),
(
"first step zero",
Rc::new(|k| Ok(Px::new(if k == 0 { 0.0 } else { 2.0 }))),
&[1.5, 10.25, 999.5],
),
];
for (name, steps, xs) in datasets {
let acc = accumulator(steps);
for &x in xs {
let back = acc.inverse(acc.forward(x).unwrap()).unwrap().get();
assert!((back - x).abs() <= 4.0 * f64::EPSILON * x, "{name}: x = {x}: {back}");
}
}
}
#[test]
fn forward_difference_inverse_sweeps_many_x_exactly() {
for steps in [Rc::new(wavy) as Steps, Rc::new(irregular)] {
let acc = accumulator(steps);
let mut worst = 0.0f64;
for k in 0..2000 {
let x = 12345.0 + k as f64 * 0.0137;
let back = acc.inverse(acc.forward(x).unwrap()).unwrap().get();
worst = worst.max((back - x).abs());
}
assert!(worst <= 1e-11, "worst roundtrip error {worst:e}");
}
}
#[test]
fn forward_difference_inverse_constant_steps_are_exact() {
let acc = accumulator(Rc::new(|_| Ok(Px::new(10.0))));
assert_eq!(acc.inverse(Px::new(25.0)).unwrap().get(), 2.5);
assert_eq!(acc.inverse(Px::new(10.0)).unwrap().get(), 1.0);
assert_eq!(acc.inverse(Px::new(0.0)).unwrap().get(), 0.0);
assert_eq!(acc.inverse(Px::new(-5.0)).unwrap().get(), -0.5);
}
#[test]
fn forward_difference_inverse_zero_step_returns_start_of_plateau() {
let acc = accumulator(Rc::new(|k| Ok(Px::new(if k == 1 { 0.0 } else { 10.0 }))));
assert_eq!(acc.inverse(Px::new(10.0)).unwrap().get(), 1.0);
assert_eq!(acc.inverse(Px::new(15.0)).unwrap().get(), 2.5);
}
#[test]
fn forward_difference_inverse_finite_domain_boundaries() {
let acc =
accumulator(Rc::new(
|k| {
if k < 3 { Ok(Px::new(10.0)) } else { Err(Error::OutOfIndex(3)) }
},
));
assert_eq!(acc.inverse(Px::new(25.0)).unwrap().get(), 2.5);
assert_eq!(acc.inverse(Px::new(30.0)).unwrap().get(), 3.0);
assert!(matches!(acc.inverse(Px::new(30.5)), Err(Error::OutOfIndex(3))));
}
#[test]
fn nan_is_rejected_as_invalid_input_for_every_definition() {
let steps: DefaultSteps = Rc::new(|_| Ok(Px::new(1.0)));
let defs = [
IncrementFunction::Scale(2.0),
IncrementFunction::VectorList(alloc::vec![Px::new(0.0), Px::new(1.0)]),
IncrementFunction::ForwardDifference(steps),
];
for def in defs {
let acc = def.accumulate().unwrap();
assert!(matches!(acc.forward(f64::NAN), Err(Error::InvalidInput)));
assert!(matches!(acc.inverse(Px::new(f64::NAN)), Err(Error::InvalidInput)));
}
let nan_scale: Result<Accumulator, _> = IncrementFunction::Scale(f64::NAN).accumulate();
assert!(matches!(nan_scale, Err(Error::InvalidInput)));
}
#[test]
fn forward_difference_negative_side_round_trips_through_the_first_step() {
let acc = accumulator(Rc::new(|i| Ok(Px::new((i + 1) as f64 * 10.0))));
for x in [-0.5, -1.0, -3.25] {
let px = acc.forward(x).unwrap();
assert_eq!(px.get(), 10.0 * x);
assert!((acc.inverse(px).unwrap().get() - x).abs() < 1e-12);
}
let flat = accumulator(Rc::new(|_| Ok(Px::new(0.0))));
assert!(matches!(flat.inverse(Px::new(-1.0)), Err(Error::OutOfIndex(0))));
assert_eq!(flat.inverse(Px::new(0.0)).unwrap().get(), 0.0);
}
#[test]
fn value_supports_comparison_debug_and_sign_operations() {
use alloc::format;
let a = Px::new(1.5);
let b = Px::new(2.5);
assert!(a < b && b > a && a <= a && a != b);
assert_eq!(a, Px::new(1.5));
assert_eq!(Px::default(), Px::new(0.0));
assert_eq!(-a, Px::new(-1.5));
assert_eq!(2.0 * a, Px::new(3.0));
assert_eq!(b / 2.0, Px::new(1.25));
let mut c = b;
c -= a;
assert_eq!(c, Px::new(1.0));
assert_eq!(format!("{:?}", a), "Value(1.5)");
assert_eq!(Px::new(f64::NAN).partial_cmp(&a), None);
assert!(Px::new(f64::NAN) != Px::new(f64::NAN));
}
#[test]
fn scale_accepts_negative_and_infinite_values() {
let acc = IncrementFunction::<DefaultSteps>::Scale(-2.0).accumulate().unwrap();
assert_eq!(acc.forward(3.0).unwrap().get(), -6.0);
assert_eq!(acc.inverse(Px::new(-6.0)).unwrap().get(), 3.0);
assert_eq!(acc.forward(f64::INFINITY).unwrap().get(), f64::NEG_INFINITY);
}
#[test]
fn forward_difference_inverse_rejects_non_finite_targets() {
let acc = accumulator(Rc::new(|_| Ok(Px::new(1.0))));
for target in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(matches!(acc.inverse(Px::new(target)), Err(Error::InvalidInput)));
}
}
#[test]
fn forward_difference_inverse_cost_is_one_call_per_segment_up_to_the_answer() {
use core::cell::Cell;
let calls = Rc::new(Cell::new(0u32));
let counter = calls.clone();
let acc = accumulator(Rc::new(move |_| {
counter.set(counter.get() + 1);
Ok(Px::new(2.0))
}));
acc.inverse(Px::new(2000.5)).unwrap(); assert_eq!(calls.get(), 1001);
}
#[test]
fn arc_pointer_makes_the_grid_send_and_sync() {
use alloc::sync::Arc;
type SyncSteps = Arc<dyn Fn(u32) -> Result<Px, Error> + Send + Sync>;
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<RectGrid<2, SyncSteps>>();
let steps: SyncSteps = Arc::new(|k| Ok(Px::new(10.0 + k as f64)));
let grid: RectGrid<2, SyncSteps> = RectGrid::new(
[Px::new(0.0), Px::new(0.0)],
[IncrementFunction::ForwardDifference(steps), IncrementFunction::Scale(2.0)],
)
.unwrap();
assert_eq!(grid.unit_to_px(0, &Unit::new(2.0)).unwrap().get(), 21.0);
assert_eq!(grid.unit_to_px(1, &Unit::new(3.0)).unwrap().get(), 6.0);
}
fn vector_list_grid(pxs: &[f64]) -> RectGrid<1> {
RectGrid::<1>::new(
[Px::new(0.0)],
[IncrementFunction::VectorList(pxs.iter().map(|&v| Px::new(v)).collect())],
)
.unwrap()
}
fn unit_box(base: f64, offset: f64) -> BBox<1> {
BBox::new([Unit::new(base)], [Unit::new(offset)])
}
#[test]
fn hit_test_box_beyond_finite_domain_never_hits() {
let grid = vector_list_grid(&[0.0, 10.0]);
let boxes = alloc::vec![unit_box(5.0, 1.0)];
for px in [-1.0, 0.0, 5.0, 10.0, 50.0] {
assert_eq!(grid.hit_test([Px::new(px)], &boxes, None), None, "px {px}");
assert_eq!(grid.hit_tests([Px::new(px)], &boxes, None), alloc::vec![false], "px {px}");
assert!(grid.hit_test_with_parameter([Px::new(px)], &boxes, None).is_none(), "px {px}");
}
let boxes = alloc::vec![unit_box(0.0, 1.0), unit_box(5.0, 1.0)];
assert_eq!(grid.hit_test([Px::new(0.0)], &boxes, None), Some(0));
assert_eq!(grid.hit_tests([Px::new(5.0)], &boxes, None), alloc::vec![true, false]);
}
#[test]
fn hit_test_box_straddling_the_domain_end_is_not_evaluable() {
let grid = vector_list_grid(&[0.0, 10.0]);
let boxes = alloc::vec![unit_box(0.5, 1.0)];
assert!(matches!(grid.box_as_px(&boxes)[0][0], Err(Error::OutOfIndex(1))));
assert_eq!(grid.hit_test([Px::new(7.0)], &boxes, None), None);
}
#[test]
fn get_parameter_of_unevaluable_box_is_err_on_that_axis() {
let grid = vector_list_grid(&[0.0, 10.0]);
for bx in [unit_box(5.0, 1.0), unit_box(0.5, 1.0)] {
let parameter = grid.get_parameter([Px::new(5.0)], bx);
assert!(matches!(parameter[0], Err(Error::OutOfIndex(1))));
}
let parameter = grid.get_parameter([Px::new(5.0)], unit_box(0.0, 1.0));
assert_eq!(parameter[0].as_ref().unwrap().get(), 0.5);
}
#[test]
fn corner_test_reports_nothing_for_an_unevaluable_box() {
let grid = vector_list_grid(&[0.0, 10.0]);
let bx = unit_box(5.0, 1.0);
for px in [0.0, 5.0, 10.0] {
assert!(corner_test(&grid, [Px::new(px)], &bx, 0.1, None).0.is_none());
assert!(corner_test(&grid, [Px::new(px)], &bx, 0.1, None).1.is_none());
}
}
#[test]
fn hit_test_extend_is_clipped_at_the_domain_end() {
let grid = vector_list_grid(&[0.0, 10.0, 30.0]);
let boxes = alloc::vec![unit_box(1.0, 1.0)];
let extend = Some(([Unit::new(0.0)], [Unit::new(5.0)]));
assert_eq!(grid.hit_test([Px::new(20.0)], &boxes, extend), Some(0));
assert_eq!(grid.hit_test([Px::new(30.0)], &boxes, extend), Some(0));
assert_eq!(grid.hit_test([Px::new(30.5)], &boxes, extend), None);
assert_eq!(grid.hit_test([Px::new(9.0)], &boxes, extend), None);
let extend = Some(([Unit::new(-0.5)], [Unit::new(0.0)]));
assert_eq!(grid.hit_test([Px::new(7.0)], &boxes, extend), Some(0));
assert_eq!(grid.hit_test([Px::new(4.0)], &boxes, extend), None);
let extend = Some(([Unit::new(0.0)], [Unit::new(5.0)]));
let (_, parameter) = grid.hit_test_with_parameter([Px::new(20.0)], &boxes, extend).unwrap();
assert_eq!(parameter[0].get(), 0.5);
}
#[test]
fn vector_list_below_the_start_is_out_of_index_zero_and_extend_clips_there() {
let grid = vector_list_grid(&[0.0, 10.0, 30.0]);
assert!(matches!(grid.unit_to_px(0, &Unit::new(-0.5)), Err(Error::OutOfIndex(0))));
assert!(matches!(grid.point_to_unit([Px::new(-1.0)])[0], Err(Error::OutOfIndex(0))));
let boxes = alloc::vec![unit_box(0.0, 1.0)];
let extend = Some(([Unit::new(-5.0)], [Unit::new(0.0)]));
assert_eq!(grid.hit_test([Px::new(0.0)], &boxes, extend), Some(0));
assert_eq!(grid.hit_test([Px::new(-1.0)], &boxes, extend), None);
}
#[test]
fn hit_test_extend_is_clipped_for_forward_difference_domains() {
let grid = RectGrid::<1>::new(
[Px::new(0.0)],
[IncrementFunction::ForwardDifference(Rc::new(|k| {
if k < 3 { Ok(Px::new(10.0)) } else { Err(Error::OutOfIndex(3)) }
}))],
)
.unwrap();
let boxes = alloc::vec![unit_box(2.0, 1.0)];
let extend = Some(([Unit::new(0.0)], [Unit::new(2.0)]));
assert_eq!(grid.hit_test([Px::new(25.0)], &boxes, extend), Some(0));
assert_eq!(grid.hit_test([Px::new(30.0)], &boxes, extend), Some(0));
assert_eq!(grid.hit_test([Px::new(31.0)], &boxes, extend), None);
assert_eq!(grid.hit_test([Px::new(0.0)], &alloc::vec![unit_box(4.0, 1.0)], None), None);
}
#[test]
fn hit_test_one_unevaluable_axis_is_enough_to_miss() {
let grid = RectGrid::<2>::new(
[Px::new(0.0), Px::new(0.0)],
[
IncrementFunction::Scale(100.0),
IncrementFunction::VectorList(alloc::vec![Px::new(0.0), Px::new(10.0)]),
],
)
.unwrap();
let beyond = alloc::vec![BBox::new(
[Unit::new(0.0), Unit::new(5.0)],
[Unit::new(1.0), Unit::new(1.0)]
)];
assert_eq!(grid.hit_test([Px::new(50.0), Px::new(0.0)], &beyond, None), None);
let inside = alloc::vec![BBox::new(
[Unit::new(0.0), Unit::new(0.0)],
[Unit::new(1.0), Unit::new(1.0)]
)];
assert_eq!(grid.hit_test([Px::new(50.0), Px::new(5.0)], &inside, None), Some(0));
}
#[test]
fn hit_test_other_evaluation_errors_also_mean_not_evaluable() {
let grid = RectGrid::<1>::new(
[Px::new(0.0)],
[IncrementFunction::ForwardDifference(Rc::new(|_| Err(Error::InvalidDefinition)))],
)
.unwrap();
let boxes = alloc::vec![unit_box(1.0, 1.0)];
assert_eq!(grid.hit_test([Px::new(0.0)], &boxes, None), None);
assert!(matches!(
grid.get_parameter([Px::new(0.0)], boxes[0])[0],
Err(Error::InvalidDefinition)
));
let extend = Some(([Unit::new(0.0)], [Unit::new(1.0)]));
assert_eq!(grid.hit_test([Px::new(0.0)], &boxes, extend), None);
}
fn assert_grid_lines_exact(name: &str, acc: &Accumulator, count: u32) {
for k in 0..count {
let px = acc.forward(k as f64).unwrap();
let back = acc.inverse(px).unwrap().get();
assert_eq!(back, k as f64, "{name}: k = {k}: {back:?}");
}
}
#[test]
fn scale_inverse_returns_grid_lines_exactly() {
for scale in [0.1, 1.0 / 3.0, 1.1, 0.7, 1.5, 64.0, 200.0, 3.0e-7, 4.0e6] {
assert_grid_lines_exact(
&alloc::format!("Scale({scale})"),
&IncrementFunction::Scale(scale).accumulate().unwrap(),
100_000,
);
}
}
#[test]
fn forward_difference_inverse_returns_grid_lines_exactly() {
for (name, steps) in [
("wavy", Rc::new(wavy) as Steps),
("growing", Rc::new(growing)),
("irregular", Rc::new(irregular)),
("constant 0.1", Rc::new(|_| Ok(Px::new(0.1)))),
("constant 1/3", Rc::new(|_| Ok(Px::new(1.0 / 3.0)))),
] {
assert_grid_lines_exact(name, &accumulator(steps), 3000);
}
}
#[test]
fn vector_list_inverse_returns_grid_lines_exactly() {
let tenths: alloc::vec::Vec<Px> = (0..2000).map(|k| Px::new(k as f64 * 0.1)).collect();
assert_grid_lines_exact(
"k * 0.1",
&IncrementFunction::VectorList(tenths).accumulate().unwrap(),
2000,
);
let mut position = 0.0;
let irregular_positions: alloc::vec::Vec<Px> = (0..2000)
.map(|k| {
if k > 0 {
position += 0.1 + (k % 7) as f64 * 0.013;
}
Px::new(position)
})
.collect();
assert_grid_lines_exact(
"irregular positions",
&IncrementFunction::VectorList(irregular_positions).accumulate().unwrap(),
2000,
);
}
#[test]
fn scale_inverse_off_the_grid_line_is_still_the_plain_quotient() {
let acc = IncrementFunction::<DefaultSteps>::Scale(0.1).accumulate().unwrap();
assert_eq!(acc.inverse(Px::new(0.3)).unwrap().get(), 0.3 / 0.1);
assert_eq!(acc.inverse(Px::new(0.25)).unwrap().get(), 2.5);
}
#[test]
fn drag_without_movement_keeps_the_box_on_a_fractional_scale() {
let grid = RectGrid::<1>::new([Px::new(0.0)], [IncrementFunction::Scale(0.1)]).unwrap();
for k in 0..1000 {
let bx = BBox::new([Unit::new(k as f64)], [Unit::new(2.0)]);
let base_px = grid.unit_to_px(0, &Unit::new(k as f64)).unwrap();
let snapped = snap_bbox_to_unit(&grid, [base_px], [Px::new(0.0)], &bx, None).unwrap();
assert_eq!(snapped.base()[0].get(), k as f64, "k = {k}");
let point =
snap_point_to_unit(&grid, [base_px], [Px::new(0.0)], [Unit::new(0.0)]).unwrap();
assert_eq!(point.base()[0].get(), k as f64, "k = {k}");
}
}
#[test]
fn drag_resize_to_a_grid_line_lands_on_that_line_on_a_fractional_scale() {
let grid = RectGrid::<1>::new([Px::new(0.0)], [IncrementFunction::Scale(0.1)]).unwrap();
let bx = BBox::new([Unit::new(2.0)], [Unit::new(3.0)]);
for k in 3..1000 {
let pointer = grid.unit_to_px(0, &Unit::new(k as f64)).unwrap();
let resized = drag_resize(&grid, [pointer], &bx, [Some(false)]).unwrap();
assert_eq!(resized.offset()[0].get(), (k - 2) as f64, "k = {k}");
}
}
#[test]
fn a_failed_axis_can_be_clamped_with_the_last_index_it_reports() {
let grid = vector_list_grid(&[0.0, 10.0, 30.0]);
let Err(Error::OutOfIndex(last)) = grid.unit_to_px(0, &Unit::new(7.0)) else {
panic!("expected OutOfIndex");
};
assert_eq!(last, 2);
assert_eq!(grid.unit_to_px(0, &Unit::new(last as f64)).unwrap().get(), 30.0);
}
#[test]
fn corner_test_reports_nothing_when_any_axis_cannot_be_evaluated() {
let grid = RectGrid::<2>::new(
[Px::new(0.0), Px::new(0.0)],
[
IncrementFunction::Scale(100.0),
IncrementFunction::VectorList(alloc::vec![Px::new(0.0), Px::new(10.0)]),
],
)
.unwrap();
let beyond = BBox::new([Unit::new(0.0), Unit::new(5.0)], [Unit::new(1.0), Unit::new(1.0)]);
let (parameter, corner) =
corner_test(&grid, [Px::new(50.0), Px::new(5.0)], &beyond, 0.1, None);
assert!(parameter.is_none() && corner.is_none());
let inside = BBox::new([Unit::new(0.0), Unit::new(0.0)], [Unit::new(1.0), Unit::new(1.0)]);
let (parameter, _) = corner_test(&grid, [Px::new(50.0), Px::new(5.0)], &inside, 0.1, None);
assert!(parameter.is_some());
}
#[test]
fn list_arguments_accept_arrays_and_sub_slices() {
let grid = vector_list_grid(&[0.0, 10.0, 30.0]);
let all = alloc::vec![unit_box(0.0, 1.0), unit_box(1.0, 1.0)];
assert_eq!(grid.hit_tests([Px::new(5.0)], &[unit_box(0.0, 1.0)], None), alloc::vec![true]);
assert_eq!(grid.hit_test([Px::new(20.0)], &all[1..], None), Some(0));
assert_eq!(grid.box_as_px(&all[..1]).len(), 1);
assert_eq!(grid.point_as_px(&[[Unit::new(1.0)]])[0][0].as_ref().unwrap().get(), 10.0);
}
}