Skip to main content

adaptive_distance_field/
lib.rs

1//! Adaptively sampled distance fields in ℝᴺ, with Lipschitz-certified pruning.
2//!
3//! An [`ADF`](adf::ADF) stores a signed distance field as a `2^N`-tree whose
4//! leaves each hold a handful of [`Primitive`](adf::Primitive)s — a field
5//! closure paired with an honest upper bound on its gradient. The tree
6//! represents their pointwise `min`, so a field composed of millions of
7//! primitives is sampled in logarithmic time by descending to the leaf covering
8//! the query point, instead of evaluating every primitive.
9//!
10//! What makes that sound rather than merely fast is the Lipschitz bound. Every
11//! decision the structure makes — whether a new primitive can be skipped,
12//! whether a stored one has become redundant, which subtrees an insertion can
13//! possibly change — is settled by branch-and-bound over
14//! `f - g ≥ (f - g)(c) - (L_f + L_g)·h`, which either *proves* a region clean or
15//! descends to a witness. A primitive is never dropped on a heuristic, only on a
16//! proof, so the stored field never deviates from the `min` over everything
17//! inserted.
18//!
19//! ```no_run
20//! use adaptive_distance_field::{
21//!   adf::{ADF, Primitive},
22//!   geometry::{Point, VectorExt},
23//!   line_search::LineSearch,
24//!   sdf::{self, SDF},
25//! };
26//!
27//! // seed a 3D field with the walls of the unit cube, positive inside
28//! let mut field = ADF::<f64, 3>::new(6, vec![Primitive::new(sdf::boundary_rect)]);
29//!
30//! // climb to a local maximum of the free space — the deepest point around
31//! let start = Point::from([0.3, 0.6, 0.5]);
32//! let peak = LineSearch::default().optimize(|p| field.sdf(p), start);
33//! let clearance = field.sdf(peak);
34//!
35//! // drop a ball there, and let the tree work out which cells that can affect
36//! let ball = move |p: Point<f64, 3>| (p - peak).length() - clearance / 2.0;
37//! field.insert_at_maximum(
38//!   adaptive_distance_field::geometry::DistPoint { point: peak, distance: clearance },
39//!   Primitive::new(ball),
40//! );
41//! ```
42//!
43//! # Layout
44//!
45//! - [`geometry`] — the vocabulary: [`Point`](geometry::Point) and
46//!   [`Vector`](geometry::Vector) (both [`nalgebra`]'s, re-exported so
47//!   downstream code can name the same versions), the axis-aligned
48//!   [`Aabb`](geometry::Aabb), [`BoundingBox`](geometry::BoundingBox), and the
49//!   [`Combinator`](geometry::Combinator) trait — every way to transform or
50//!   combine one field into another. Each combinator preserves or `max`-combines
51//!   its operands' constants, so a composed chain still reports an honest bound.
52//! - [`sdf`] — the [`SDF`](sdf::SDF) and [`Lipschitz`](sdf::Lipschitz) traits
53//!   everything is written against, the field types the combinators return, and
54//!   [`sdf_geq_everywhere`](adf::sdf_geq_everywhere): the branch-and-bound
55//!   proof that underpins all of it. [`boundary_rect`](sdf::boundary_rect) seeds
56//!   a field with the walls of the unit cube.
57//! - [`adf`] — the structure itself, over the [`quadtree`](adf::quadtree) arena.
58//! - [`line_search`] — adaptive gradient ascent, for locating the maxima that
59//!   [`insert_at_maximum`](adf::ADF::insert_at_maximum) consumes.
60//!
61//! Dimension count is a compile-time constant throughout, so `ADF<f64, 2>` and
62//! `ADF<f64, 3>` monomorphize separately and the 2D case costs exactly what a
63//! quadtree-only implementation would.
64//!
65//! # Beyond this crate
66//!
67//! The [`space-filling`](https://docs.rs/space-filling/) crate builds on this
68//! one: a catalogue of shape primitives in 2, 3, 4 and N dimensions, a discrete
69//! bitmap solver for exact global maxima, batched parallel maxima search, and
70//! 2D rasterization.
71
72#![allow(clippy::type_complexity)]
73#![cfg_attr(docsrs, feature(doc_cfg))]
74
75/// Re-exported: the geometry vocabulary is nalgebra's, so downstream code can
76/// name the exact same versions.
77pub use nalgebra;
78
79pub mod adf;
80pub mod geometry;
81pub mod line_search;
82pub mod sdf;