Skip to content

Commit

Permalink
Update documentation; fix lints
Browse files Browse the repository at this point in the history
  • Loading branch information
FredericaBernkastel committed Jun 14, 2023
1 parent 5f035bf commit 8ce2c14
Show file tree
Hide file tree
Showing 18 changed files with 71 additions and 228 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "space-filling"
version = "0.3.1"
version = "0.4.0"
description = "Generalized 2D space filling"
readme = "readme.md"
authors = ["Frederica Bernkastel <bernkastel.frederica@protonmail.com>"]
Expand Down
8 changes: 5 additions & 3 deletions examples/gd_adf/02_random_distribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use {
space_filling::{
geometry::{Shape, Circle, Translation, Scale, P2},
sdf::{self, SDF},
solver::{line_search::LineSearch, adf::ADF},
solver::{LineSearch, ADF},
drawing::Draw,
util
},
Expand All @@ -17,7 +17,7 @@ use {

type AffineT<T> = Scale<Translation<T, f64>, f64>;

// profile: 62ms, 1000 circrles, adf_subdiv = 5
// profile: 62ms, 1000 circrles, adf_subdiv = 5, gd_lattice = 1
fn random_distribution(representation: &RwLock<ADF<f64>>) -> impl Iterator<Item = AffineT<Circle>> + '_ {
let mut rng = rand_pcg::Pcg64::seed_from_u64(0);

Expand Down Expand Up @@ -47,7 +47,9 @@ fn random_distribution(representation: &RwLock<ADF<f64>>) -> impl Iterator<Item

fn main() -> Result<()> {
let path = "out.png";
let representation = RwLock::new(ADF::new(5, vec![Arc::new(sdf::boundary_rect)]));
let representation = RwLock::new(
ADF::new(5, vec![Arc::new(sdf::boundary_rect)])
.with_gd_lattice_density(3)); // set ADF to a high precision
let mut image = image::RgbaImage::new(2048, 2048);

random_distribution(&representation)
Expand Down
2 changes: 1 addition & 1 deletion examples/gd_adf/04_polymorphic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use {
space_filling::{
geometry::{Shape, Ring, Square},
sdf::{self, SDF},
solver::{adf::ADF, line_search::LineSearch},
solver::{ADF, LineSearch},
drawing::{self, Draw},
util
},
Expand Down
2 changes: 1 addition & 1 deletion examples/gd_adf/06_custom_primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use {
space_filling::{
sdf::{self, SDF},
solver::{adf::ADF, line_search::LineSearch},
solver::{ADF, LineSearch},
drawing::Draw,
geometry::{WorldSpace, BoundingBox, Shape, Scale, Translation},
util
Expand Down
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[<img alt="crates.io" src="https://img.shields.io/crates/v/space-filling.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/space-filling)
[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-space--filling-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K" height="20">](https://docs.rs/space-filling)

Note: this is a part of ongoing research, no stability guarantees are made.
Note: this is a subject of ongoing research, no stability guarantees are made.

You can read this paper for introduction:
[Paul Bourke - Random space filling of the plane (2011)](http://paulbourke.net/fractals/randomtile/).
Expand All @@ -17,11 +17,11 @@ A generic and parallel interface was implemented. Currently supported:
- Any shapes which can be represented with SDF: curves, regular polygons, non-convex and disjoint areas, fractals or any sets with non-integer hausdorff dimension (as long as the distance can be approximated).

### Argmax2D
SDF is stored as a discrete bitmap, with memory layout reminiscent of z-order curve. On each iteration, the solver is guaranteed to find **global maxima**, but increasing precision requires quadratic memory.
SDF is stored as a discrete bitmap, with memory layout reminiscent of z-order curve. Solver is guaranteed to always find **global maxima**, but increasing precision requires quadratic memory.

### GD-ADF
A paper "Adaptively Sampled Distance Fields" (doi:[10.1145/344779.344899](https://dl.acm.org/doi/10.1145/344779.344899)) offered an idea of reducing memory consumption, however it was very elaborate to not include _any_ hints for a practical implementation. The only one being - using polynomial approximations for every node of the k-d tree; however, current work takes a different path - by storing function primitives themselves in each node (bucket). Redundant primitive elimination within a bucket was performed using [interior-point method](https://en.wikipedia.org/wiki/Interior-point_method).
ADF itself implements SDF trait, allowing for complex fields composed of millions of primitives to be sampled efficiently — as opposed to computing it directly (with quadratic complexity in nature).
ADF itself implements SDF trait, allowing for complex fields composed of millions of primitives to be sampled efficiently — as opposed to computing it directly with quadratic complexity in nature.
However, primitive elimination is not yet perfect. An elimination algorithm which is both precise and fast enough will be a subject of further theoretical research.

Once the representation is obtained, the role of optimizer takes place. For practical purposes, gradient descent with exponential convergence has been chosen — making GD-ADF a **local maxima** algorithm; as described by following equation:
Expand Down
4 changes: 2 additions & 2 deletions src/drawing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use {
mod impl_draw_rgbaimage;
#[cfg(test)] mod tests;

pub trait Draw<Prec, Backend>: Shape<Prec> {
pub trait Draw<Float, Backend>: Shape<Float> {
fn draw(&self, image: &mut Backend);
}

Expand All @@ -37,7 +37,7 @@ impl <B, S, P> Draw<P, B> for Scale<S, P> where Scale<S, P>: Shape<P> {

impl <B, P> Draw<P, B> for geometry::Line<P> where geometry::Line<P>: Shape<P> {
fn draw(&self, _: &mut B) { unreachable!("{}", MSG) } }
impl <B, P, U> Draw<P, B> for geometry::Polygon<U> where P: num_traits::Float, U: AsRef<[Point2D<P, WorldSpace>]> {
impl <B, P, U> Draw<P, B> for geometry::Polygon<U> where P: Float, U: AsRef<[Point2D<P, WorldSpace>]> {
fn draw(&self, _: &mut B) { unreachable!("{}", MSG) } }

#[derive(Debug, Copy, Clone)]
Expand Down
32 changes: 3 additions & 29 deletions src/geometry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
//! interval `[-1, 1]`, and center in the origin.

use {
std::ops::{Add, Mul},
euclid::{Point2D, Box2D, Vector2D as V2, Size2D, Rotation2D, Angle},
num_traits::{NumCast, Float},
std::ops::Add,
euclid::{Point2D, Box2D, Vector2D as V2, Rotation2D, Angle},
num_traits::Float,
crate::sdf::{SDF, Union, Subtraction, Intersection, SmoothMin}
};

Expand Down Expand Up @@ -118,32 +118,6 @@ impl <T, S> BoundingBox<T> for Scale<S, T>
}
}

pub fn to_world_space<T, U>(
point: Point2D<T, PixelSpace>,
resolution: Size2D<T, PixelSpace>
) -> Point2D<U, WorldSpace>
where T: NumCast + Copy,
U: NumCast + Copy + std::ops::Div<Output = U>
{
point.cast::<U>().to_vector()
.component_div(resolution.cast::<U>().to_vector())
.cast_unit()
.to_point()
}

pub fn to_pixel_space<T, U>(
point: Point2D<T, WorldSpace>,
resolution: Size2D<U, PixelSpace>
) -> Point2D<U, PixelSpace>
where T: NumCast + Copy + Mul<Output = T>,
U: NumCast + Copy
{
point.to_vector().component_mul(resolution.to_vector().cast().cast_unit())
.cast_unit()
.to_point()
.cast::<U>()
}

fn update_bounding_box<T>(
bounding: Box2D<T, WorldSpace>,
morphism: impl Fn(Point2D<T, WorldSpace>) -> Point2D<T, WorldSpace>
Expand Down
23 changes: 16 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,11 @@
//! ```
//! <img src="https://raw.githubusercontent.com/FredericaBernkastel/space-filling/master/doc/fractal_distribution.png">
//!
//! # On dynamic dispatch and parallelism
//! There are three main traits related to drawing:
//! # Drawing
//! Drawing was intended to be a rudimentary module for displaying sdf shapes. It is not highly
//! optimized, and you are free to use third-party libraries for this purpose.
//!
//! There are two traits related to drawing:
//! - `trait `[`Shape`](`geometry::Shape`)`: `[`SDF`](`sdf::SDF`)` + `[`BoundingBox`](`geometry::BoundingBox`)
//! - `trait `[`Draw`](`drawing::Draw`)`:`[`Shape`](`geometry::Shape`)`
//!
Expand All @@ -136,23 +139,29 @@
//! .draw(...);
//! }
//! ```
//! But this won't work, because all of `Shape` methods require `Sized`.
//! But this won't work, because all of `Shape` methods require `Sized`, hence no object safe.
//! Correct way is:
//! ```ignore
//! let shapes: Vec<Box<dyn Draw<RgbaImage>>> = ...
//! ```
//! However, since Rust never got a support of trait upcasting, we cannot obtain `dyn Shape` from
//! `dyn Draw`, hence somewhat limited in capabilities.
//!
//! Lastly, there are is: [`draw_parallel`](drawing::draw_parallel), that accept an iterator on
//! `dyn Draw<RgbaImage> + Send + Sync`. It is constructed via trait object casting, exactly as above.
//! See `examples/polymorphic.rs` and `drawing/tests::polymorphic_*` for more examples.
//! Lastly, there is: [`draw_parallel`](drawing::draw_parallel), which is convenient when shapes
//! require heavy computations to draw, such as texture loading. It accepts an iterator on
//! `dyn Draw<RgbaImage> + Send + Sync`, constructed via trait object casting, exactly as above.
//! See `examples/argmax2d/03_embedded.rs`, `examples/gd_adf/04_polymorphic.rs` and
//! `drawing/tests::polymorphic_*` for more details.
//!
//! This way, both distribution generation and drawing are guaranteed to evenly load all available
//! cores, as long as enough memory bandwidth is available.
//! cores.
//!
//! Have a good day, `nyaa~ =^_^=`
//!
//! <img src="https://raw.githubusercontent.com/FredericaBernkastel/space-filling/master/doc/neko.gif">

#![allow(clippy::type_complexity)]

#![cfg_attr(doc, feature(doc_cfg))]
#![allow(rustdoc::private_intra_doc_links)]

Expand Down
2 changes: 1 addition & 1 deletion src/sdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl <S, P: Float> SDF<P> for Translation<S, P>
where S: Shape<P>,
P: Clone + Sub<Output = P> {
fn sdf(&self, pixel: Point2D<P, WorldSpace>) -> P {
self.shape.sdf(pixel - self.offset.clone())
self.shape.sdf(pixel - self.offset)
}
}

Expand Down
31 changes: 19 additions & 12 deletions src/solver/adf/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! Adaptive Distance Field, uses quadtree as an underlying data structire.
//! Each node (bucket) stores several `Arc<dyn Fn(Point2D) -> {float}>`

#![allow(clippy::mut_from_ref)]
use {
crate::{
solver::{
line_search::LineSearch
},
solver::LineSearch,
geometry::{Shape, shapes, P2, WorldSpace, BoundingBox},
sdf::SDF,
},
Expand Down Expand Up @@ -56,7 +58,7 @@ fn sdf_partialord<_Float: Float + Signed>(

let control_points = |rect: Rect<_, _>| {
let p = (0..lattice_density).map(move |x| _Float::from(x).unwrap() / _Float::from(lattice_density - 1).unwrap());
itertools::iproduct!(p.clone(), p.clone())
itertools::iproduct!(p.clone(), p)
.map(move |p| rect.origin + rect.size.to_vector().component_mul(p.into()))
};

Expand All @@ -73,20 +75,21 @@ fn sdf_partialord<_Float: Float + Signed>(
}

impl <_Float: Float + Signed + Sync> ADF<_Float> {
//move |p| self.tree.data.as_slice().sdf(p)
/// Create a new ADF instance. `max_depth` specifies maximum number of quadtree subdivisions;
/// `init` specifies initial sdf primitives.
pub fn new(max_depth: u8, init: Vec<Arc<dyn Fn(P2<_Float>) -> _Float>>) -> Self {
Self {
tree: Quadtree::new(max_depth, init),
ipm_gd_lattice_density: 1,
ipm_line_config: LineSearch::default()
}
}
/// Controls precision of bucket primitive pruning
/// Controls precision of primitive pruning in a bucket.
pub fn with_gd_lattice_density(mut self, density: u32) -> Self {
self.ipm_gd_lattice_density = density;
self
}
/// Underlying GD settings for the interior point method
/// Underlying GD settings for the interior point method (a part of primitive pruning).
pub fn with_ipm_line_config(mut self, line_config: LineSearch<_Float>) -> Self {
self.ipm_line_config = line_config;
self
Expand Down Expand Up @@ -123,14 +126,15 @@ impl <_Float: Float + Signed + Sync> ADF<_Float> {
let control_points = |rect: Rect<_, _>| {
let n = 5;
let p = (0..n).map(move |x| x as f64 / (n - 1) as f64);
itertools::iproduct!(p.clone(), p.clone())
itertools::iproduct!(p.clone(), p)
.map(move |p| rect.origin + rect.size.to_vector().component_mul(p.into()))
};

!control_points(d)
.any(|v| g(v) > f(v))
}

/// Add a new sdf primitive function.
pub fn insert_sdf_domain(&mut self, domain: Rect<_Float, WorldSpace>, f: Arc<dyn Fn(P2<_Float>) -> _Float + Send + Sync>) -> bool {
let change_exists = AtomicBool::new(false);

Expand Down Expand Up @@ -197,19 +201,20 @@ impl <_Float: Float + Signed + Sync> ADF<_Float> {
};

// max tree depth is reached, just append the primitive
if node.depth == node.max_depth {
if node.depth == node.max_depth || node.data.len() < BUCKET_SIZE {

node.data.push(f.clone());
//node.data = prune(node.data.as_slice(), node.rect);

} else if node.data.len() < BUCKET_SIZE {
} /*else if node.data.len() < BUCKET_SIZE {
//node.data = prune(node.data.as_slice(), node.rect);
//if !Self::higher_all(f.as_ref(), &node.sdf_vec(), node.rect) {
node.data.push(f.clone());
// node.data.push(f.clone());
//}
} else { // max bucket size is reached, subdivide
}*/
else { // max bucket size is reached, subdivide

let mut g = node.data.clone();
g.push(f.clone());
Expand All @@ -230,6 +235,8 @@ impl <_Float: Float + Signed + Sync> ADF<_Float> {
change_exists.load(Ordering::SeqCst)
}

/// # Safety
/// Nobody is safe
pub unsafe fn as_mut(&self) -> &mut Self {
let ptr = self as *const _ as usize;
&mut *(ptr as *const Self as *mut _)
Expand Down
2 changes: 1 addition & 1 deletion src/solver/adf/quadtree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl Quadtrant {
.component_mul(rect.size.to_vector());
Rect { origin, size: rect.size / (_Float::one() + _Float::one()) }
.contains(pt)
.then(|| quad)
.then_some(quad)
})
}

Expand Down
4 changes: 1 addition & 3 deletions src/solver/adf/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ use {
geometry::{Circle, Shape, P2},
drawing,
sdf,
solver::{
adf::ADF, line_search::LineSearch
},
solver::{ADF, LineSearch},
util
},
anyhow::Result,
Expand Down
4 changes: 4 additions & 0 deletions src/solver/argmax2d/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Discrete distance field representation. Uses an f32-bitmap to store the data.

use {
crate::{
geometry::{DistPoint, PixelSpace, WorldSpace}
Expand Down Expand Up @@ -34,6 +36,7 @@ impl Argmax2D {
unsafe { *(ptr as *const DistPoint<f32, f32, WorldSpace> as *mut _) = dist }
}

/// Find global maxima.
pub fn find_max(&self) -> DistPoint<f32, f32, WorldSpace> {
*self.chunk_argmax.iter()
.max()
Expand Down Expand Up @@ -85,6 +88,7 @@ impl Argmax2D {
});
}

/// Read underlying distance field bitmap.
pub fn pixels(&self) -> impl Iterator<Item = DistPoint<f32, u64, PixelSpace>> + '_ {
self.dist_map.pixels()
}
Expand Down
3 changes: 1 addition & 2 deletions src/solver/argmax2d/z_order_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ impl <T: Clone> ZOrderStorage<Vec<T>> {

pub fn chunks(&self) -> impl Iterator<Item = Chunk<T>> {
let chunk_count = (self.resolution / self.chunk_size).pow(2);
(0..chunk_count).into_iter()
.map(move |id| self.get_chunk(id))
(0..chunk_count).map(move |id| self.get_chunk(id))
}

pub fn pixel(&self, xy: Point2D<u64, PixelSpace>) -> T {
Expand Down
Loading

0 comments on commit 8ce2c14

Please sign in to comment.