From 032c77c11e92d49e902313fc70a86847dda0c6de Mon Sep 17 00:00:00 2001 From: Stefan Altmayer Date: Sun, 26 Nov 2023 13:32:07 +0100 Subject: [PATCH] Implements natural neighbor interpolation --- CHANGELOG.md | 7 + Cargo.toml | 5 +- README.md | 30 +- benches/benchmarks.rs | 4 +- benches/interpolation_benchmark.rs | 47 ++ examples/interpolation/main.rs | 214 +++++ examples/svg_renderer/main.rs | 20 +- examples/svg_renderer/quicksketch/mod.rs | 12 + examples/svg_renderer/scenario.rs | 11 +- examples/svg_renderer/scenario_list.rs | 254 +++++- images/grid_3d.svg | 12 + images/interpolation_barycentric.img | 1 + images/interpolation_barycentric.jpg | Bin 0 -> 30155 bytes images/interpolation_nearest_neighbor.img | 1 + images/interpolation_nearest_neighbor.jpg | Bin 0 -> 27251 bytes images/interpolation_nn_c0.img | 1 + images/interpolation_nn_c0.jpg | Bin 0 -> 28937 bytes images/interpolation_nn_c1.img | 1 + images/interpolation_nn_c1.jpg | Bin 0 -> 29109 bytes images/natural_neighbor_insertion_cell.svg | 156 ++++ images/natural_neighbor_polygon.svg | 185 +++++ images/natural_neighbor_scenario.svg | 140 ++++ images/preview.html | 2 +- src/delaunay_core/bulk_load.rs | 4 +- src/delaunay_core/hint_generator.rs | 2 +- src/delaunay_core/interpolation.rs | 895 +++++++++++++++++++++ src/delaunay_core/math.rs | 72 +- src/delaunay_core/mod.rs | 1 + src/delaunay_core/triangulation_ext.rs | 2 +- src/delaunay_triangulation.rs | 25 +- src/lib.rs | 2 + src/triangulation.rs | 10 + 32 files changed, 2075 insertions(+), 41 deletions(-) create mode 100644 benches/interpolation_benchmark.rs create mode 100644 examples/interpolation/main.rs create mode 100644 images/grid_3d.svg create mode 100644 images/interpolation_barycentric.img create mode 100644 images/interpolation_barycentric.jpg create mode 100644 images/interpolation_nearest_neighbor.img create mode 100644 images/interpolation_nearest_neighbor.jpg create mode 100644 images/interpolation_nn_c0.img create mode 100644 images/interpolation_nn_c0.jpg create mode 100644 images/interpolation_nn_c1.img create mode 100644 images/interpolation_nn_c1.jpg create mode 100644 images/natural_neighbor_insertion_cell.svg create mode 100644 images/natural_neighbor_polygon.svg create mode 100644 images/natural_neighbor_scenario.svg create mode 100644 src/delaunay_core/interpolation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 24c1a33..d6e6518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2.5.0] - yyyy-mm-dd + +### Added + - Implements natural neighbor interpolation. See type `NaturalNeighbor` for more on this! +### Fix + - Fixes `PointProjection::reversed()` - the method would return a projection that gives sometimes incorrect results. + ## [2.4.1] - 2023-12-01 ### Fix diff --git a/Cargo.toml b/Cargo.toml index 863b1d0..9b4274f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,11 @@ rand = "0.8.3" cgmath = "0.18.0" svg = "0.14.0" float_next_after = "1" +image = "0.24.7" +tiny-skia = "0.11.3" criterion = { version = "0.5.1", features = ["html_reports"] } - +base64 = "0.21.5" +anyhow = "1.0.75" [[bench]] name = "benchmarks" diff --git a/README.md b/README.md index 1146322..f39fc3d 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,15 @@ Delaunay triangulations for the rust ecosystem. -- 2D [Delaunay triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation), optionally backed by a hierarchy - structure for improved nearest neighbor and insertion performance. -- Allows both incremental and bulk loading creation of triangulations -- Support for vertex removal -- 2D constrained Delaunay triangulation (CDT) -- [Delaunay refinement](https://en.wikipedia.org/wiki/Delaunay_refinement) -- Uses precise geometric predicates to prevent incorrect geometries due to rounding issues -- Supports extracting the Voronoi diagram + - 2D [Delaunay triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation), optionally backed + by a hierarchy structure for improved nearest neighbor and insertion performance. + - Allows both incremental and bulk loading creation of triangulations + - Support for vertex removal + - 2D constrained Delaunay triangulation (CDT) + - [Delaunay refinement](https://en.wikipedia.org/wiki/Delaunay_refinement) + - Uses precise geometric predicates to prevent incorrect geometries due to rounding issues + - Supports extracting the Voronoi diagram + - Natural neighbor interpolation --------------------------- @@ -31,19 +32,10 @@ Project goals, in the order of their importance: # Roadmap -For Spade 2.x: - - Add back the removed interpolation methods (natural neighbor interpolation, #67) - For Spade 3: - - Possibly base `spade` on `nalgebra` as underlying vector and matrix library. Not much else planned yet! - -# Project state and contributing - -Looking for co-maintainers! Projects with just a single maintainer can be a little unreliable due to the single point of failure. I would love to see this burden being distributed on more shoulders! This is less about *implementing* things but rather about general maintenance tasks, e.g. package updates, minor bug fixes, reviewing PRs, etc... - -If you want to contribute, please consider opening an issue first. I'm happy to help out with any questions! + - Possibly API simplification by un-supporting non-f64 outputs. -# Performance and comparison to other Delaunay crates +# Performance and comparison to other crates Refer to [the delaunay_compare readme](./delaunay_compare/README.md) for some benchmarks and a comparison with other crates. diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 4b11b85..707cc13 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -5,15 +5,17 @@ use criterion::*; mod benchmark_utilities; mod bulk_load_benchmark; mod bulk_load_vs_incremental; +mod interpolation_benchmark; mod locate_benchmark; criterion_group! { name = benches; config = Criterion::default().sample_size(50).warm_up_time(Duration::from_secs(1)); targets = - locate_benchmark::locate_benchmark, bulk_load_benchmark::bulk_load_benchmark, bulk_load_vs_incremental::bulk_load_vs_incremental_benchmark, + interpolation_benchmark::interpolation_benchmark, + locate_benchmark::locate_benchmark, } criterion_main!(benches); diff --git a/benches/interpolation_benchmark.rs b/benches/interpolation_benchmark.rs new file mode 100644 index 0000000..a352ab8 --- /dev/null +++ b/benches/interpolation_benchmark.rs @@ -0,0 +1,47 @@ +use criterion::*; +use spade::Triangulation; +use spade::{DelaunayTriangulation, FloatTriangulation, Point2}; + +use crate::benchmark_utilities::*; + +pub fn interpolation_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("interpolation benchmarks"); + + let points = uniform_distribution(*SEED, 1.0) + .take(50) + .collect::>(); + let triangulation = DelaunayTriangulation::<_>::bulk_load(points).unwrap(); + + let query_point = Point2::new(0.5, -0.2); + + group.bench_function("nearest neighbor interpolation", |b| { + b.iter(|| { + triangulation + .nearest_neighbor(query_point) + .unwrap() + .position() + }); + }); + + let barycentric = triangulation.barycentric(); + group.bench_function("barycentric interpolation", |b| { + b.iter(|| barycentric.interpolate(|v| v.position().x + v.position().y, query_point)); + }); + + let natural_neighbor = &triangulation.natural_neighbor(); + group.bench_function("natural neighbor interpolation (c0)", |b| { + b.iter(|| natural_neighbor.interpolate(|v| v.position().x + v.position().y, query_point)); + }); + + group.bench_function("natural neighbor interpolation (c1)", |b| { + b.iter(|| { + natural_neighbor.interpolate_gradient( + |v| v.position().x + v.position().y, + |_| [0.0, 0.0], + 0.5, + query_point, + ) + }); + }); + group.finish(); +} diff --git a/examples/interpolation/main.rs b/examples/interpolation/main.rs new file mode 100644 index 0000000..1fc9526 --- /dev/null +++ b/examples/interpolation/main.rs @@ -0,0 +1,214 @@ +use anyhow::*; +use image::{ImageBuffer, Rgba}; +use std::io::{Cursor, Write}; + +use base64::Engine; +use tiny_skia::*; + +use spade::{DelaunayTriangulation, FloatTriangulation, HasPosition, Point2, Triangulation}; + +#[derive(Debug, Copy, Clone)] +struct PointWithHeight { + position: Point2, + height: f64, +} + +impl PointWithHeight { + const fn new(x: f64, y: f64, height: f64) -> Self { + Self { + position: Point2::new(x, y), + height, + } + } +} + +impl HasPosition for PointWithHeight { + type Scalar = f64; + + fn position(&self) -> Point2 { + self.position + } +} + +type TriangulationType = DelaunayTriangulation; + +const VERTICES: &[PointWithHeight] = &[ + PointWithHeight::new(20.0, 20.0, 0.9), + PointWithHeight::new(20.0, -20.0, 0.9), + PointWithHeight::new(-20.0, 20.0, 0.9), + PointWithHeight::new(-20.0, -20.0, 0.9), + PointWithHeight::new(20.0, 10.0, 0.0), + PointWithHeight::new(10.0, 10.0, 0.5), + PointWithHeight::new(0.0, 23.0, 0.1), + PointWithHeight::new(0.0, -23.0, 0.1), + PointWithHeight::new(-10.0, 10.0, 0.0), + PointWithHeight::new(-10.0, -10.0, 0.1), + PointWithHeight::new(-15.0, 20.0, 0.5), + PointWithHeight::new(-20.0, 20.0, 0.0), + PointWithHeight::new(-5.0, 0.0, 0.25), + PointWithHeight::new(5.0, 7.0, 0.75), + PointWithHeight::new(12.0, -10.0, 0.4), + PointWithHeight::new(5.0, 10.0, 0.3), + PointWithHeight::new(5.0, 1.0, 0.2), +]; + +pub fn main() -> anyhow::Result<()> { + let mut t = TriangulationType::default(); + for vertex in VERTICES { + t.insert(*vertex)?; + } + + const OFFSET: f32 = 25.0; + const SCALAR: f32 = 50.0 / 512.0; + + fn px_to_coords(x: u32, y: u32) -> Point2 { + Point2::new( + x as f64 * SCALAR as f64 - OFFSET as f64, + y as f64 * SCALAR as f64 - OFFSET as f64, + ) + } + + let dimensions = 512; + let mut nn_c0_pixmap = + Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?; + let mut nn_c1_pixmap = + Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?; + let mut barycentric_pixmap = + Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?; + let mut nearest_neighbor_pixmap = + Pixmap::new(dimensions, dimensions).context("Failed to allocate image")?; + + fn set_pixel(pixmap: &mut Pixmap, x: u32, y: u32, value: Option) { + let background_color = [255, 255, 255]; + let [r, g, b] = value.map(float_to_color).unwrap_or(background_color); + let base = (y * pixmap.height() + x) as usize * 4; + pixmap.data_mut()[base] = r; + pixmap.data_mut()[base + 1] = g; + pixmap.data_mut()[base + 2] = b; + // Alpha + pixmap.data_mut()[base + 3] = 255; + } + + let nn = t.natural_neighbor(); + let barycentric = t.barycentric(); + + for y in 0..dimensions { + for x in 0..dimensions { + let coords = px_to_coords(x, y); + let value_nn_c0 = nn.interpolate(|v| v.data().height, coords); + set_pixel(&mut nn_c0_pixmap, x, y, value_nn_c0); + + let value_nn_c1 = + nn.interpolate_gradient(|v| v.data().height, |_| [0.0, 0.0], 1.0, coords); + set_pixel(&mut nn_c1_pixmap, x, y, value_nn_c1); + + let value_barycentric = barycentric.interpolate(|v| v.data().height, coords); + set_pixel(&mut barycentric_pixmap, x, y, value_barycentric); + + let value_nearest_neighbor = t.nearest_neighbor(coords).map(|v| v.data().height); + set_pixel(&mut nearest_neighbor_pixmap, x, y, value_nearest_neighbor); + } + } + + let path = { + let mut pb = PathBuilder::new(); + + for edge in t.undirected_edges() { + let [from, to] = edge.positions(); + pb.move_to(from.x as f32, from.y as f32); + pb.line_to(to.x as f32, to.y as f32); + } + + pb.finish().context("Could not build path")? + }; + + let stroke = Stroke { + width: 0.15, + ..Default::default() + }; + + let mut paint = Paint::default(); + paint.set_color_rgba8(0, 0, 0, 70); + paint.anti_alias = true; + + for pixmap in [ + &mut nn_c0_pixmap, + &mut nn_c1_pixmap, + &mut barycentric_pixmap, + &mut nearest_neighbor_pixmap, + ] { + pixmap.stroke_path( + &path, + &paint, + &stroke, + Transform::from_translate(OFFSET, OFFSET).post_scale(1.0 / SCALAR, 1.0 / SCALAR), + None, + ); + } + + fn save_pixmap(pixmap: Pixmap, name: &str) -> anyhow::Result<()> { + // tiny_skia doesn't support jpg encoding which is required for small file size when embedding this into + // the documentation. We'll have to convert the data into ImageBuffer from the image crate and then do + // the jpeg encoding. + let (width, height) = (pixmap.width(), pixmap.height()); + let data = pixmap.take(); + let buffer = ImageBuffer::, _>::from_vec(width, height, data) + .context("Failed to convert to ImageBuffer")?; + + let mut data_jpeg: Cursor> = Cursor::new(Vec::new()); + buffer.write_to(&mut data_jpeg, image::ImageFormat::Jpeg)?; + + std::fs::write(format!("images/{}.jpg", name), data_jpeg.get_ref())?; + + // Encode image as tag for inclusion into the documentation + let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(data_jpeg.get_ref()); + let mut file = std::fs::File::create(format!("images/{}.img", name))?; + write!(file, r#""#, encoded)?; + Ok(()) + } + + save_pixmap(nn_c0_pixmap, "interpolation_nn_c0")?; + save_pixmap(nn_c1_pixmap, "interpolation_nn_c1")?; + save_pixmap(barycentric_pixmap, "interpolation_barycentric")?; + save_pixmap(nearest_neighbor_pixmap, "interpolation_nearest_neighbor")?; + + Ok(()) +} + +fn float_to_color(value: f64) -> [u8; 3] { + // mostly AI generated.. + // Converts a hue value in the range 0.0 ..= 1.0 from HLS to RGB + let value = value.clamp(0.0, 1.0); + + const LIGHTNESS: f64 = 0.45; + const SATURATION: f64 = 0.55; + + let c = (1.0 - (2.0 * LIGHTNESS - 1.0).abs()) * SATURATION; + + let hue = value * 360.0; + let hs = hue / 60.0; + + let x = c * (1.0 - (hs % 2.0 - 1.0).abs()); + let m = LIGHTNESS - c * 0.5; + + let (r, g, b) = if (0.0..60.0).contains(&hue) { + (c, x, 0.0) + } else if (60.0..120.0).contains(&hue) { + (x, c, 0.0) + } else if (120.0..180.0).contains(&hue) { + (0.0, c, x) + } else if (180.0..240.0).contains(&hue) { + (0.0, x, c) + } else if (240.0..300.0).contains(&hue) { + (x, 0.0, c) + } else { + (c, 0.0, x) + }; + + // Convert RGB to 8-bit values + let r = ((r + m) * 255.0) as u8; + let g = ((g + m) * 255.0) as u8; + let b = ((b + m) * 255.0) as u8; + + [r, g, b] +} diff --git a/examples/svg_renderer/main.rs b/examples/svg_renderer/main.rs index 2b8a8db..febb032 100644 --- a/examples/svg_renderer/main.rs +++ b/examples/svg_renderer/main.rs @@ -1,12 +1,26 @@ pub mod quicksketch; mod scenario; mod scenario_list; - -type Result = core::result::Result<(), Box>; +use anyhow::Result; /// Used for rendering SVGs for documentation. These are inlined (via #[doc = include_str!(...)]) /// into the doc comment of a few items. That makes sure they will be visible even for offline users. -fn main() -> Result { +fn main() -> Result<()> { + scenario_list::natural_neighbor_area_scenario(false)?.save_to_svg( + "natural_neighbor_insertion_cell", + "images/natural_neighbor_insertion_cell.svg", + )?; + + scenario_list::natural_neighbor_area_scenario(true)?.save_to_svg( + "natural_neighbor_polygon", + "images/natural_neighbor_polygon.svg", + )?; + + scenario_list::natural_neighbors_scenario().save_to_svg( + "natural_neighbor_scenario", + "images/natural_neighbor_scenario.svg", + )?; + scenario_list::refinement_maximum_area_scenario(None).save_to_svg( "refinement_maximum_area_no_limit", "images/refinement_maximum_area_no_limit.svg", diff --git a/examples/svg_renderer/quicksketch/mod.rs b/examples/svg_renderer/quicksketch/mod.rs index 1909ca4..dd86461 100644 --- a/examples/svg_renderer/quicksketch/mod.rs +++ b/examples/svg_renderer/quicksketch/mod.rs @@ -125,6 +125,7 @@ pub struct Style { stroke_color: Option, stroke_style: Option, fill: Option, + opacity: Option, marker_start: Option, marker_end: Option, } @@ -144,6 +145,11 @@ impl Style { .as_ref() .map(|color| format!("stroke: {}", color)); + let opacity = self + .opacity + .as_ref() + .map(|opacity| format!("opacity: {:.2}", opacity)); + let fill = format!( "fill: {}", self.fill @@ -175,6 +181,7 @@ impl Style { stroke_color, marker_start, marker_end, + opacity, stroke_dash_array, ]) .flatten() @@ -356,6 +363,11 @@ impl SketchPath { self } + pub fn opacity(mut self, opacity: f64) -> Self { + self.style.opacity = Some(opacity); + self + } + pub fn fill(mut self, fill: SketchFill) -> Self { self.style.fill = Some(fill); self diff --git a/examples/svg_renderer/scenario.rs b/examples/svg_renderer/scenario.rs index c7e8b51..5791676 100644 --- a/examples/svg_renderer/scenario.rs +++ b/examples/svg_renderer/scenario.rs @@ -3,9 +3,10 @@ use spade::{ConstrainedDelaunayTriangulation, DelaunayTriangulation, HasPosition use crate::convert_point; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Debug)] pub struct VertexType { position: Point, + pub color: Option, pub radius: f64, } @@ -21,6 +22,7 @@ impl VertexType { Self { position: Point::new(x, y), + color: None, radius: DEFAULT_CIRCLE_RADIUS, } } @@ -34,6 +36,7 @@ impl HasPosition for VertexType { } } +#[derive(Clone, Copy, Debug)] pub struct UndirectedEdgeType { pub color: SketchColor, } @@ -52,7 +55,7 @@ impl Default for UndirectedEdgeType { } } -#[derive(Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct DirectedEdgeType {} #[derive(Clone, Copy, Debug)] @@ -166,7 +169,9 @@ where for vertex in triangulation.vertices() { sketch.add_with_layer( SketchElement::circle(convert_point(vertex.position()), vertex.data().radius) - .fill(SketchFill::solid(options.vertex_color)) + .fill(SketchFill::solid( + vertex.data().color.unwrap_or(options.vertex_color), + )) .stroke_width(0.5) .stroke_color(options.vertex_stroke_color), crate::quicksketch::SketchLayer::VERTICES, diff --git a/examples/svg_renderer/scenario_list.rs b/examples/svg_renderer/scenario_list.rs index 042848f..55ea56b 100644 --- a/examples/svg_renderer/scenario_list.rs +++ b/examples/svg_renderer/scenario_list.rs @@ -3,13 +3,17 @@ use super::quicksketch::{ SketchLayer, StrokeStyle, Vector, }; +use anyhow::{Context, Result}; + use cgmath::{Angle, Bounded, Deg, EuclideanSpace, InnerSpace, Point2, Vector2}; + use spade::{ handles::{ FixedDirectedEdgeHandle, VoronoiVertex::{self, Inner, Outer}, }, - AngleLimit, FloatTriangulation as _, InsertionError, RefinementParameters, Triangulation as _, + AngleLimit, FloatTriangulation as _, HasPosition, InsertionError, RefinementParameters, + Triangulation as _, }; use crate::{ @@ -974,6 +978,254 @@ pub fn shape_iterator_scenario(use_circle_metric: bool, iterate_vertices: bool) result } +pub fn natural_neighbors_scenario() -> Sketch { + let triangulation = big_triangulation().unwrap(); + + let nn = triangulation.natural_neighbor(); + + let mut nns = Vec::new(); + let query_point = spade::Point2::new(-1.0, 4.0); + nn.get_weights(query_point, &mut nns); + + let mut result = convert_triangulation(&triangulation, &Default::default()); + + let offsets = [ + Vector2::new(2.0, 5.0), + Vector2::new(-4.0, 6.0), + Vector2::new(0.0, 6.0), + Vector2::new(1.0, 6.0), + Vector2::new(0.0, 8.0), + Vector2::new(3.0, 4.0), + Vector2::new(5.0, 2.0), + ]; + + for (index, (neighbor, weight)) in nns.iter().enumerate() { + let neighbor = triangulation.vertex(*neighbor); + let position = convert_point(neighbor.position()); + result.add( + SketchElement::circle(position, 2.0) + .fill(SketchFill::Solid(SketchColor::SALMON)) + .stroke_width(0.5) + .stroke_color(SketchColor::BLACK), + ); + result.add( + SketchElement::text(index.to_string()) + .position(position) + .font_size(3.0) + .horizontal_alignment(HorizontalAlignment::Middle) + .dy(1.15), + ); + + result.add( + SketchElement::text(format!("{:.2}", weight)) + .position(position + offsets[index]) + .font_size(5.0), + ); + } + + result.add( + SketchElement::circle(convert_point(query_point), 1.5) + .fill(SketchFill::Solid(SketchColor::ROYAL_BLUE)) + .stroke_width(0.2) + .stroke_color(SketchColor::BLACK), + ); + + result.set_view_box_min(Point2::new(-40.0, -30.0)); + result.set_view_box_max(Point2::new(30.0, 45.0)); + result.set_width(400); + result +} + +/// Only used for internal documentation of natural neighbor area calculation +pub fn natural_neighbor_area_scenario(include_faces: bool) -> Result { + let mut triangulation: Triangulation = Default::default(); + + triangulation.insert(VertexType::new(45.0, 30.0))?; + triangulation.insert(VertexType::new(7.5, 40.0))?; + triangulation.insert(VertexType::new(-45.0, 42.0))?; + triangulation.insert(VertexType::new(-55.0, 0.0))?; + triangulation.insert(VertexType::new(-32.0, -42.0))?; + triangulation.insert(VertexType::new(-2.0, -32.0))?; + triangulation.insert(VertexType::new(25.0, -32.0))?; + + triangulation.insert(VertexType::new(70.0, 40.0))?; + triangulation.insert(VertexType::new(20.0, 65.0))?; + triangulation.insert(VertexType::new(-60.0, 50.0))?; + triangulation.insert(VertexType::new(-70.0, -15.5))?; + triangulation.insert(VertexType::new(-50.0, -60.0))?; + triangulation.insert(VertexType::new(-9.0, -70.0))?; + triangulation.insert(VertexType::new(42.0, -50.0))?; + + for edge in triangulation.fixed_undirected_edges() { + triangulation.undirected_edge_data_mut(edge).color = SketchColor::LIGHT_GRAY; + } + + for face in triangulation.fixed_inner_faces() { + triangulation.face_data_mut(face).fill = SketchFill::solid(SketchColor::ANTIQUE_WHITE); + } + + let query_vertex = VertexType::new(-5.0, -5.0); + let mut nns = Vec::new(); + triangulation + .natural_neighbor() + .get_weights(query_vertex.position(), &mut nns); + + for (vertex, _) in &nns { + triangulation.vertex_data_mut(*vertex).color = Some(SketchColor::CRIMSON); + } + + let mut result = convert_triangulation(&triangulation, &Default::default()); + + for (index, (vertex, _)) in nns.iter().enumerate() { + let vertex = triangulation.vertex(*vertex); + result.add( + SketchElement::text(format!("{index}")) + .position(convert_point(vertex.position())) + .font_size(2.5) + .horizontal_alignment(HorizontalAlignment::Middle) + .dy(0.9), + ); + } + + for edge in triangulation.undirected_voronoi_edges() { + let [v0, v1] = edge.vertices(); + + if let (Some(v0), Some(v1)) = (v0.position(), v1.position()) { + result.add( + SketchElement::line(convert_point(v0), convert_point(v1)) + .stroke_color(SketchColor::SALMON) + .stroke_width(0.5), + ); + } + } + + let mut circumcenters = Vec::new(); + for face in triangulation.inner_faces() { + let circumcenter = convert_point(face.circumcenter()); + circumcenters.push(circumcenter); + + result.add( + SketchElement::circle(circumcenter, 1.0) + .fill(SketchFill::Solid(SketchColor::ROYAL_BLUE)), + ); + } + + if !include_faces { + result.add( + SketchElement::circle(convert_point(query_vertex.position()), 1.0) + .fill(SketchFill::Solid(SketchColor::RED)), + ); + } + + let mut insertion_cell_points = Vec::new(); + let inserted = triangulation.insert(query_vertex)?; + for edge in triangulation + .vertex(inserted) + .as_voronoi_face() + .adjacent_edges() + { + let context = "Edge was infinite - is insertion position correct?"; + let from = edge.from().position().context(context)?; + let to = edge.to().position().context(context)?; + insertion_cell_points.push(convert_point(from)); + + result.add( + SketchElement::line(convert_point(from), convert_point(to)) + .stroke_color(SketchColor::ORANGE_RED), + ); + } + for cell_point in &insertion_cell_points { + result.add( + SketchElement::circle(*cell_point, 1.0) + .fill(SketchFill::solid(SketchColor::DARK_GREEN)), + ); + } + + if include_faces { + let nn3 = convert_point(triangulation.vertex(nns[3].0).position()); + let nn4 = convert_point(triangulation.vertex(nns[4].0).position()); + let nn5 = convert_point(triangulation.vertex(nns[5].0).position()); + + let last_edge = SketchElement::line(nn3, nn4) + .with_arrow_end(ArrowType::FilledArrow) + .stroke_color(SketchColor::ROYAL_BLUE) + .shift_from(-2.3) + .shift_to(-7.0); + + result.add( + last_edge + .create_adjacent_text("last_edge") + .font_size(3.0) + .dy(3.5), + ); + result.add(last_edge); + let stop_edge = SketchElement::line(nn4, nn5) + .with_arrow_end(ArrowType::FilledArrow) + .stroke_color(SketchColor::ROYAL_BLUE) + .shift_from(-2.3) + .shift_to(-7.0); + result.add( + stop_edge + .create_adjacent_text("stop_edge") + .font_size(3.0) + .dy(3.5), + ); + result.add(stop_edge); + + result.add( + SketchElement::text("first") + .position(insertion_cell_points[6] + Vector2::new(0.0, 0.0)) + .dy(3.3) + .font_size(2.5), + ); + result.add( + SketchElement::text("last") + .position(insertion_cell_points[0] + Vector2::new(-5.5, 0.0)) + .dy(3.0) + .font_size(2.5), + ); + + result.add( + SketchElement::text("c2") + .position(circumcenters[4] + Vector2::new(-3.5, 0.0)) + .dy(-1.0) + .font_size(2.5), + ); + + result.add( + SketchElement::text("c1") + .position(circumcenters[2]) + .dy(-2.0) + .font_size(2.5), + ); + + result.add( + SketchElement::text("c0") + .position(circumcenters[3] + Vector2::new(1.5, 0.0)) + .dy(0.5) + .font_size(2.5), + ); + + let path = SketchElement::path() + .move_to(insertion_cell_points[6]) + .line_to(insertion_cell_points[0]) + .line_to(circumcenters[4]) + .line_to(circumcenters[2]) + .line_to(circumcenters[3]) + .close() + .fill(SketchFill::solid(SketchColor::ORANGE)) + .opacity(0.75); + + result.add_with_layer(path, SketchLayer::EDGES); + } + + result + .set_view_box_min(Point2::new(-60.0, -45.0)) + .set_view_box_max(Point2::new(45.0, 60.0)); + + Ok(result) +} + pub fn refinement_scenario(do_refine: bool) -> Sketch { let mut cdt = create_refinement_cdt(); diff --git a/images/grid_3d.svg b/images/grid_3d.svg new file mode 100644 index 0000000..cfcfbe6 --- /dev/null +++ b/images/grid_3d.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/images/interpolation_barycentric.img b/images/interpolation_barycentric.img new file mode 100644 index 0000000..91a7dab --- /dev/null +++ b/images/interpolation_barycentric.img @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/interpolation_barycentric.jpg b/images/interpolation_barycentric.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea36e8538d7c96eed1b3889deedd55e6c65e613d GIT binary patch literal 30155 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 Z|M4Q}|KZ>Dx5D}Vg3I6dL%2Uj{s;SwYX$%S literal 0 HcmV?d00001 diff --git a/images/interpolation_nearest_neighbor.img b/images/interpolation_nearest_neighbor.img new file mode 100644 index 0000000..aa91cc0 --- /dev/null +++ b/images/interpolation_nearest_neighbor.img @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/interpolation_nearest_neighbor.jpg b/images/interpolation_nearest_neighbor.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a935bf82ce5371050e459965380b6a89ff32b28 GIT binary patch literal 27251 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 V@T!wzlmvY`Rr?=*1pe{Oe*hUb0`C9- literal 0 HcmV?d00001 diff --git a/images/interpolation_nn_c0.img b/images/interpolation_nn_c0.img new file mode 100644 index 0000000..9412ea4 --- /dev/null +++ b/images/interpolation_nn_c0.img @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/interpolation_nn_c0.jpg b/images/interpolation_nn_c0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..15419600b1868b4180e10657ca63290575eeb506 GIT binary patch literal 28937 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 \ No newline at end of file diff --git a/images/interpolation_nn_c1.jpg b/images/interpolation_nn_c1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bb196daf7246b028826f8b1055955389a688cf5f GIT binary patch literal 29109 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 CAJkm{ literal 0 HcmV?d00001 diff --git a/images/natural_neighbor_insertion_cell.svg b/images/natural_neighbor_insertion_cell.svg new file mode 100644 index 0000000..caf83c6 --- /dev/null +++ b/images/natural_neighbor_insertion_cell.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 + + +1 + + +2 + + +3 + + +4 + + +5 + + +6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/natural_neighbor_polygon.svg b/images/natural_neighbor_polygon.svg new file mode 100644 index 0000000..a31d799 --- /dev/null +++ b/images/natural_neighbor_polygon.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 + + +1 + + +2 + + +3 + + +4 + + +5 + + +6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +last_edge + + + +stop_edge + + + +first + + +last + + +c2 + + +c1 + + +c0 + + + \ No newline at end of file diff --git a/images/natural_neighbor_scenario.svg b/images/natural_neighbor_scenario.svg new file mode 100644 index 0000000..3aa4db6 --- /dev/null +++ b/images/natural_neighbor_scenario.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 + + +0.27 + + + +1 + + +0.48 + + + +2 + + +0.01 + + + +3 + + +0.04 + + + +4 + + +0.19 + + + +5 + + +0.00 + + + +6 + + +0.01 + + + + \ No newline at end of file diff --git a/images/preview.html b/images/preview.html index 0097e83..a7da86c 100644 --- a/images/preview.html +++ b/images/preview.html @@ -23,7 +23,7 @@ } - + \ No newline at end of file diff --git a/src/delaunay_core/bulk_load.rs b/src/delaunay_core/bulk_load.rs index ad60be9..63d4f0a 100644 --- a/src/delaunay_core/bulk_load.rs +++ b/src/delaunay_core/bulk_load.rs @@ -33,9 +33,9 @@ impl Eq for FloatOrd {} /// Advances in Engineering Software, /// Volume 43, Issue 1, /// 2012, -/// https://doi.org/10.1016/j.advengsoft.2011.09.003 +/// /// -/// Or alternatively: http://cglab.ca/~biniaz/papers/Sweep%20Circle.pdf +/// Or alternatively: /// /// # Overview /// diff --git a/src/delaunay_core/hint_generator.rs b/src/delaunay_core/hint_generator.rs index 412c598..07b4626 100644 --- a/src/delaunay_core/hint_generator.rs +++ b/src/delaunay_core/hint_generator.rs @@ -20,7 +20,7 @@ use alloc::vec::Vec; /// site. /// /// Hints can also be given manually by using the `...with_hint` methods (e.g. -/// [Triangulation::insert_with_hint](crate::Triangulation::insert_with_hint)) +/// [Triangulation::insert_with_hint]) /// /// Usually, you should not need to implement this trait. Spade currently implements two common hint generators that should /// fulfill most needs: diff --git a/src/delaunay_core/interpolation.rs b/src/delaunay_core/interpolation.rs new file mode 100644 index 0000000..d10f414 --- /dev/null +++ b/src/delaunay_core/interpolation.rs @@ -0,0 +1,895 @@ +use core::cell::RefCell; + +use crate::{ + delaunay_core::math, + handles::{FixedDirectedEdgeHandle, FixedVertexHandle}, + DelaunayTriangulation, HasPosition, HintGenerator, Point2, PositionInTriangulation, + Triangulation, +}; +use num_traits::{one, zero, Float}; + +use alloc::vec::Vec; + +use super::VertexHandle; + +/// Implements methods for natural neighbor interpolation. +/// +/// Natural neighbor interpolation is a spatial interpolation method. For a given set of 2D input points with an +/// associated value (e.g. a height field of some terrain or temperature measurements at different locations in +/// a country), natural neighbor interpolation allows to smoothly interpolate the associated value for every +/// location within the convex hull of the input points. +/// +/// Spade currently assists with 4 interpolation strategies: +/// - **Nearest neighbor interpolation:** Fastest. Exhibits too poor quality for many tasks. Not continuous +/// along the edges of the voronoi diagram. Use [DelaunayTriangulation::nearest_neighbor]. +/// - **Barycentric interpolation:** Fast. Not smooth on the edges of the Delaunay triangulation. +/// See [Barycentric]. +/// - **Natural neighbor interpolation:** Slower. Smooth everywhere except the input points. The input points +/// have no derivative. See [NaturalNeighbor::interpolate] +/// - **Natural neighbor interpolation with gradients:** Slowest. Smooth everywhere, even at the input points. +/// See [NaturalNeighbor::interpolate_gradient]. +/// +/// # Performance comparison +/// +/// In general, the speed of interpolating random points in a triangulation will be determined by the speed of the +/// lookup operation after a certain triangulation size is reached. Thus, if random sampling is required, using a +/// [crate::HierarchyHintGenerator] may be beneficial. +/// +/// If subsequent queries are close to each other, [crate::LastUsedVertexHintGenerator] should also work fine. +/// In this case, interpolating a single value should result in the following (relative) run times: +/// +/// | nearest neighbor | barycentric | natural neighbor (no gradients) | natural neighbor (gradients) | +/// | ---------------- | ----------- | ------------------------------- | ---------------------------- | +/// | 23ns | 103ns | 307ns | 422ns | +/// +/// # Usage +/// +/// This type is created by calling [DelaunayTriangulation::natural_neighbor]. It contains a few internal buffers +/// that are used to prevent recurring allocations. For best performance it should be created only once per thread +/// and then used in all interpolation activities (see example). +/// +/// # Example +/// ``` +/// use spade::{Point2, HasPosition, DelaunayTriangulation, InsertionError, Triangulation as _}; +/// +/// struct PointWithHeight { +/// position: Point2, +/// height: f64, +/// } +/// +/// impl HasPosition for PointWithHeight { +/// type Scalar = f64; +/// +/// fn position(&self) -> Point2 { +/// self.position +/// } +/// } +/// +/// fn main() -> Result<(), InsertionError> { +/// let mut t = DelaunayTriangulation::::new(); +/// t.insert(PointWithHeight { position: Point2::new(-1.0, -1.0), height: 42.0})?; +/// t.insert(PointWithHeight { position: Point2::new(-1.0, 1.0), height: 13.37})?; +/// t.insert(PointWithHeight { position: Point2::new(1.0, -1.0), height: 27.18})?; +/// t.insert(PointWithHeight { position: Point2::new(1.0, 1.0), height: 31.41})?; +/// +/// // Set of query points (would be many more realistically): +/// let query_points = [Point2::new(0.0, 0.1), Point2::new(0.5, 0.1)]; +/// +/// // Good: Re-use interpolation object! +/// let nn = t.natural_neighbor(); +/// for p in &query_points { +/// println!("Value at {:?}: {:?}", p, nn.interpolate(|v| v.data().height, *p)); +/// } +/// +/// // Bad (slower): Don't re-use internal buffers +/// for p in &query_points { +/// println!("Value at {:?}: {:?}", p, t.natural_neighbor().interpolate(|v| v.data().height, *p)); +/// } +/// Ok(()) +/// } +/// ``` +/// +/// # Visual comparison of interpolation algorithms +/// +/// *Note: All of these images are generated by the "interpolation" example* +/// +/// Nearest neighbor interpolation exhibits discontinuities along the voronoi edges: +/// +#[doc = include_str!("../../images/interpolation_nearest_neighbor.img")] +/// +/// Barycentric interpolation, by contrast, is continuous everywhere but has no derivative on the edges of the +/// Delaunay triangulation. These show up as sharp corners in the color gradients on the drawn edges: +/// +#[doc = include_str!("../../images/interpolation_barycentric.img")] +/// +/// By contrast, natural neighbor interpolation is smooth on the edges - the previously sharp angles are now rounded +/// off. However, the vertices themselves are still not continuous and will form sharp "peaks" in the resulting +/// surface. +/// +#[doc = include_str!("../../images/interpolation_nn_c0.img")] +/// +/// With a gradient, the sharp peaks are gone - the surface will smoothly approximate a linear function as defined +/// by the gradient in the vicinity of each vertex. In the image below, a gradient of `(0.0, 0.0)` is used which +/// leads to a small "disc" around each vertex with values close to the vertex value. +/// +#[doc = include_str!("../../images/interpolation_nn_c1.img")] +/// +#[doc(alias = "Interpolation")] +pub struct NaturalNeighbor<'a, T> +where + T: Triangulation, + T::Vertex: HasPosition, +{ + triangulation: &'a T, + // Various buffers that are used for identifying natural neighbors and their weights. It's significantly faster + // to clear and re-use these buffers instead of allocating them anew. + // We also don't run the risk of mutably borrowing them twice (causing a RefCell panic) as the RefCells never leak + // any of the interpolation methods. + // Not implementing `Sync` is also fine as the workaround - creating a new `NaturalNeighbor` instance per thread - + // is very simple. + inspect_edges_buffer: RefCell>, + natural_neighbor_buffer: RefCell>, + insert_cell_buffer: RefCell::Scalar>>>, + weight_buffer: RefCell::Scalar)>>, +} + +/// Implements methods related to barycentric interpolation. +/// +/// Created by calling [crate::FloatTriangulation::barycentric]. +/// +/// Refer to the documentation of [NaturalNeighbor] for an overview of different interpolation methods. +#[doc(alias = "Interpolation")] +pub struct Barycentric<'a, T> +where + T: Triangulation, +{ + triangulation: &'a T, + weight_buffer: RefCell::Scalar)>>, +} + +impl<'a, T> Barycentric<'a, T> +where + T: Triangulation, + ::Scalar: Float, +{ + pub(crate) fn new(triangulation: &'a T) -> Self { + Self { + triangulation, + weight_buffer: Default::default(), + } + } + + /// Returns the barycentric coordinates and the respective vertices for a given query position. + /// + /// The resulting coordinates and vertices are stored within the given `result` `vec`` to prevent + /// unneeded allocations. `result` will be cleared initially. + /// + /// The number of returned elements depends on the query positions location: + /// - `result` will be **empty** if the query position lies outside of the triangulation's convex hull + /// - `result` will contain **a single element** (with weight 1.0) if the query position lies exactly on a vertex + /// - `result` will contain **two vertices** if the query point lies exactly on any edge of the triangulation. + /// - `result` will contain **exactly three** elements if the query point lies on an inner face of the + /// triangulation. + pub fn get_weights( + &self, + position: Point2<::Scalar>, + result: &mut Vec<(FixedVertexHandle, ::Scalar)>, + ) { + result.clear(); + match self.triangulation.locate(position) { + PositionInTriangulation::OnVertex(vertex) => { + result.push((vertex, ::Scalar::from(1.0))) + } + PositionInTriangulation::OnEdge(edge) => { + let [v0, v1] = self.triangulation.directed_edge(edge).vertices(); + let [w0, w1] = two_point_interpolation::(v0, v1, position); + result.push((v0.fix(), w0)); + result.push((v1.fix(), w1)); + } + PositionInTriangulation::OnFace(face) => { + let face = self.triangulation.face(face); + let [v0, v1, v2] = face.vertices(); + let [c0, c1, c2] = face.barycentric_interpolation(position); + result.extend([(v0.fix(), c0), (v1.fix(), c1), (v2.fix(), c2)]); + } + _ => {} + } + } + + /// Performs barycentric interpolation on this triangulation at a given position. + /// + /// Returns `None` for any value outside the triangulation's convex hull. + /// The value to interpolate is given by the `i` parameter. + /// + /// Refer to [NaturalNeighbor] for a comparison with other interpolation methods. + pub fn interpolate( + &self, + i: I, + position: Point2<::Scalar>, + ) -> Option<::Scalar> + where + I: Fn( + VertexHandle, + ) -> ::Scalar, + { + let nns = &mut *self.weight_buffer.borrow_mut(); + self.get_weights(position, nns); + if nns.is_empty() { + return None; + } + + let mut total_sum = zero(); + for (vertex, weight) in nns { + total_sum = total_sum + i(self.triangulation.vertex(*vertex)) * *weight; + } + Some(total_sum) + } +} + +impl<'a, V, DE, UE, F, L> NaturalNeighbor<'a, DelaunayTriangulation> +where + V: HasPosition, + DE: Default, + UE: Default, + F: Default, + L: HintGenerator<::Scalar>, + ::Scalar: Float, +{ + pub(crate) fn new(triangulation: &'a DelaunayTriangulation) -> Self { + Self { + triangulation, + inspect_edges_buffer: Default::default(), + insert_cell_buffer: Default::default(), + natural_neighbor_buffer: Default::default(), + weight_buffer: Default::default(), + } + } + + /// Calculates the natural neighbors and their weights (sibson coordinates) of a given query position. + /// + /// The neighbors are returned in clockwise order. The weights will add up to 1.0. + /// The neighbors are stored in the `result` parameter to prevent unnecessary allocations. + /// `result` will be cleared initially. + /// + /// The number of returned natural neighbors depends on the given query position: + /// - `result` will be **empty** if the query position lies outside of the triangulation's convex hull + /// - `result` will contain **exactly one** vertex if the query position is equal to that vertex position. + /// - `result` will contain **exactly two** entries if the query position lies exactly *on* an edge of the + /// convex hull. + /// - `result` will contain **at least three** `(vertex, weight)` tuples if the query point lies on an inner + /// face or an inner edge. + /// + /// *Example: The natural neighbors (red vertices) of the query point (blue dot) with their weights. + /// The elements will be returned in clockwise order as indicated by the indices drawn within the red circles.* + /// + #[doc = include_str!("../../images/natural_neighbor_scenario.svg")] + pub fn get_weights( + &self, + position: Point2<::Scalar>, + result: &mut Vec<(FixedVertexHandle, ::Scalar)>, + ) { + let nns = &mut *self.natural_neighbor_buffer.borrow_mut(); + get_natural_neighbor_edges( + self.triangulation, + &mut self.inspect_edges_buffer.borrow_mut(), + position, + nns, + ); + self.get_natural_neighbor_weights(position, nns, result); + } + + /// Interpolates a value at a given position. + /// + /// Returns `None` for any point outside the triangulations convex hull. + /// The value to interpolate is given by the `i` parameter. The resulting interpolation will be smooth + /// everywhere except at the input vertices. + /// + /// Refer to [NaturalNeighbor] for an example on how to use this function. + pub fn interpolate( + &self, + i: I, + position: Point2<::Scalar>, + ) -> Option<::Scalar> + where + I: Fn(VertexHandle) -> ::Scalar, + { + let nns = &mut *self.weight_buffer.borrow_mut(); + self.get_weights(position, nns); + if nns.is_empty() { + return None; + } + + let mut total_sum = zero(); + for (vertex, weight) in nns { + total_sum = total_sum + i(self.triangulation.vertex(*vertex)) * *weight; + } + Some(total_sum) + } + + /// Interpolates a value at a given position. + /// + /// In contrast to [Self::interpolate], this method has a well defined derivative at each vertex and will + /// approximate a linear function in the proximity of any vertex. + /// + /// The value to interpolate is given by the `i` parameter. The gradient that defines the derivative at + /// each input vertex is given by the `g` parameter. + /// + /// The `flatness` parameter blends between an interpolation that ignores the given gradients (value 0.0) + /// or adheres to it strongly (values larger than ~2.0) in the vicinity of any vertex. When in doubt, using + /// a value of 1.0 should result in a good interpolation and is also the fastest. + /// + /// Returns `None` for any point outside of the triangulation's convex hull. + /// + /// Refer to [NaturalNeighbor] for more information and a visual example. + /// + /// # Example + /// + /// ``` + /// use spade::{DelaunayTriangulation, HasPosition, Point2}; + /// # use spade::Triangulation; + /// + /// struct PointWithHeight { + /// position: Point2, + /// height: f64, + /// } + /// + /// impl HasPosition for PointWithHeight { + /// type Scalar = f64; + /// fn position(&self) -> Point2 { self.position } + /// } + /// + /// let mut triangulation: DelaunayTriangulation = Default::default(); + /// // Insert some points into the triangulation + /// triangulation.insert(PointWithHeight { position: Point2::new(10.0, 10.0), height: 0.0 }); + /// triangulation.insert(PointWithHeight { position: Point2::new(10.0, -10.0), height: 0.0 }); + /// triangulation.insert(PointWithHeight { position: Point2::new(-10.0, 10.0), height: 0.0 }); + /// triangulation.insert(PointWithHeight { position: Point2::new(-10.0, -10.0), height: 0.0 }); + /// + /// let nn = triangulation.natural_neighbor(); + /// + /// // Interpolate point at coordinates (1.0, 2.0). This example uses a fixed gradient of (0.0, 0.0) which + /// // means that the interpolation will have normal vector parallel to the z-axis at each input point. + /// // Realistically, the gradient might be stored as an additional property of `PointWithHeight`. + /// let query_point = Point2::new(1.0, 2.0); + /// let value: f64 = nn.interpolate_gradient(|v| v.data().height, |_| [0.0, 0.0], 1.0, query_point).unwrap(); + /// ``` + /// + /// # References + /// + /// This method uses the C1 extension proposed by Sibson in + /// "A brief description of natural neighbor interpolation, R. Sibson, 1981" + pub fn interpolate_gradient( + &self, + i: I, + g: G, + flatness: ::Scalar, + position: Point2<::Scalar>, + ) -> Option<::Scalar> + where + I: Fn(VertexHandle) -> ::Scalar, + G: Fn(VertexHandle) -> [::Scalar; 2], + { + let nns = &mut *self.weight_buffer.borrow_mut(); + self.get_weights(position, nns); + if nns.is_empty() { + return None; + } + + // Variable names should make more sense after looking into the paper! + // Roughly speaking, this approach works by blending a smooth c1 approximation into the + // regular natural neighbor interpolation ("c0 contribution"). + // The c0 / c1 contributions are stored in sum_c0 / sum_c1 and are weighted by alpha and beta + // respectively. + let mut sum_c0 = zero(); + let mut sum_c1 = zero(); + let mut sum_c1_weights = zero(); + let mut alpha: ::Scalar = zero(); + let mut beta: ::Scalar = zero(); + + for (handle, weight) in nns { + let handle = self.triangulation.vertex(*handle); + let pos_i = handle.position(); + let h_i = i(handle); + let diff = pos_i.sub(position); + let r_i2 = diff.length2(); + let r_i = r_i2.powf(flatness); + let c1_weight_i = *weight / r_i; + let grad_i = g(handle); + let zeta_i = h_i + diff.dot(grad_i.into()); + alpha = alpha + c1_weight_i * r_i; + beta = beta + c1_weight_i * r_i2; + sum_c1_weights = sum_c1_weights + c1_weight_i; + sum_c1 = sum_c1 + zeta_i * c1_weight_i; + sum_c0 = sum_c0 + h_i * *weight; + } + alpha = alpha / sum_c1_weights; + sum_c1 = sum_c1 / sum_c1_weights; + let result = (alpha * sum_c0 + beta * sum_c1) / (alpha + beta); + + Some(result) + } + + /// Calculates the natural neighbor weights corresponding to a given position. + /// + /// The weight of a natural neighbor n is defined as the size of the intersection of two areas: + /// - The existing voronoi cell of the natural neighbor + /// - The cell that would be created if a vertex was created at the given position. + /// + /// The area of this intersection can, surprisingly, be calculated without doing the actual insertion. + /// + /// Parameters: + /// - position refers to the position for which the weights should be calculated + /// - nns: A list of edges that connect the natural neighbors (e.g. `nns[0].to() == nns[1].from()`). + /// - result: Stores the resulting NNs (as vertex handle) and their weights. + /// + /// # Visualization + /// + /// Refer to these two .svg files for more detail on how the algorithm works, either by looking them up + /// directly or by running `cargo doc --document-private-items` and looking at the documentation of this + /// function. + /// + /// ## Insertion cell + /// + /// This .svg displays the *insertion cell* (thick orange line) which is the voronoi cell that gets + /// created if the query point (red dot) would be inserted. + /// Each point of the insertion cell lies on a circumcenter of a triangle formed by `position` and two + /// adjacent natural neighbors (red dots with their index shown inside). + #[doc = include_str!("../../images/natural_neighbor_insertion_cell.svg")] + /// + /// ## Inner loop + /// + /// This .svg illustrates the inner loop of the algorithm (see code below). The goal is to calculate + /// the weight of natural neighbor with index 4 which is proportional to the area of the orange polygon. + /// `last_edge`, `stop_edge`, `first`, `c0`, `c1` `c2` and `last` refer to variable names (see code below). + #[doc = include_str!("../../images/natural_neighbor_polygon.svg")] + fn get_natural_neighbor_weights( + &self, + position: Point2<::Scalar>, + nns: &[FixedDirectedEdgeHandle], + result: &mut Vec<(FixedVertexHandle, ::Scalar)>, + ) { + result.clear(); + + if nns.is_empty() { + return; + } + + if nns.len() == 1 { + let edge = self.triangulation.directed_edge(nns[0]); + result.push((edge.from().fix(), one())); + return; + } + + if nns.len() == 2 { + let [e0, e1] = [ + self.triangulation.directed_edge(nns[0]), + self.triangulation.directed_edge(nns[1]), + ]; + let [v0, v1] = [e0.from(), e1.from()]; + let [w0, w1] = + two_point_interpolation::>(v0, v1, position); + + result.push((v0.fix(), w0)); + result.push((v1.fix(), w1)); + return; + } + + // Get insertion cell vertices. The "insertion cell" refers to the voronoi cell that would be + // created if "position" would be inserted. + // These insertion cells happen to lie on the circumcenter of `position` and any two adjacent + // natural neighbors (e.g. [position, nn[2].position(), nn[3].position()]). + // + // `images/natural_neighbor_insertion_cell.svg` depicts the cell as thick orange line. + let mut insertion_cell = self.insert_cell_buffer.borrow_mut(); + insertion_cell.clear(); + for cur_nn in nns { + let cur_nn = self.triangulation.directed_edge(*cur_nn); + + let [from, to] = cur_nn.positions(); + insertion_cell.push(math::circumcenter([to, from, position]).0); + } + + let mut total_area = zero(); // Used to normalize weights at the end + + let mut last_edge = self.triangulation.directed_edge(*nns.last().unwrap()); + let mut last = *insertion_cell.last().unwrap(); + + for (stop_edge, first) in core::iter::zip(nns.iter(), &*insertion_cell) { + // Main loop + // + // Refer to images/natural_neighbor_polygon.svg for some visual aid. + // + // The outer loops calculates the weight of an individual natural neighbor. + // To do this, it calculates the intersection area of the insertion cell with the cell of the + // current natural neighbor. + // This intersection is a convex polygon with vertices `first, current0, current1 ... last` + // (ordered ccw, `currentX` refers to the variable in the inner loop) + // + // The area of a convex polygon [v0 ... vN] is given by + // 0.5 * ((v0.x * v1.y + ... vN.x * v0.y) - (v0.y * v1.x + ... vN.y * v0.x)) + // ⮤ positive_area ⮥ ⮤ negative_area ⮥ + // + // The positive and negative contributions are calculated separately to avoid precision issues. + // The factor of 0.5 can be omitted as the weights are normalized anyway. + + // `stop_edge` is used to know when to stop the inner loop (= once the polygon is finished) + let stop_edge = self.triangulation.directed_edge(*stop_edge); + assert!(!stop_edge.is_outer_edge()); + + let mut positive_area = first.x * last.y; + let mut negative_area = first.y * last.x; + + loop { + // All other polygon vertices happen lie on the circumcenter of a face adjacent to an + // out edge of the current natural neighbor. + // + // The natural_neighbor_polygon.svg refers to this variable as `c0`, `c1`, and `c2`. + let current = last_edge.face().as_inner().unwrap().circumcenter(); + positive_area = positive_area + last.x * current.y; + negative_area = negative_area + last.y * current.x; + + last_edge = last_edge.next().rev(); + last = current; + + if last_edge == stop_edge.rev() { + positive_area = positive_area + current.x * first.y; + negative_area = negative_area + current.y * first.x; + break; + } + } + + let polygon_area = positive_area - negative_area; + + total_area = total_area + polygon_area; + result.push((stop_edge.from().fix(), polygon_area)); + + last = *first; + last_edge = stop_edge; + } + + for tuple in result { + tuple.1 = tuple.1 / total_area; + } + } +} + +fn get_natural_neighbor_edges( + triangulation: &T, + inspect_buffer: &mut Vec, + position: Point2<::Scalar>, + result: &mut Vec, +) where + T: Triangulation, + ::Scalar: Float, +{ + inspect_buffer.clear(); + result.clear(); + match triangulation.locate(position) { + PositionInTriangulation::OnFace(face) => { + for edge in triangulation + .face(face) + .adjacent_edges() + .into_iter() + .rev() + .map(|e| e.rev()) + { + inspect_flips(triangulation, result, inspect_buffer, edge.fix(), position); + } + } + PositionInTriangulation::OnEdge(edge) => { + let edge = triangulation.directed_edge(edge); + + if edge.is_part_of_convex_hull() { + result.extend([edge.fix(), edge.fix().rev()]); + return; + } + + for edge in [edge, edge.rev()] { + inspect_flips(triangulation, result, inspect_buffer, edge.fix(), position); + } + } + PositionInTriangulation::OnVertex(fixed_handle) => { + let vertex = triangulation.vertex(fixed_handle); + result.push( + vertex + .out_edge() + .map(|e| e.fix()) + .unwrap_or(FixedDirectedEdgeHandle::new(0)), + ) + } + _ => {} + } + result.reverse(); +} + +/// Identifies natural neighbors. +/// +/// To do so, this function "simulates" the insertion of a vertex (located at `position) and keeps track of which +/// edges would need to be flipped. A vertex is a natural neighbor if it happens to be part of an edge that would +/// require to be flipped. +/// +/// Similar to function `legalize_edge` (which is used for *actual* insertions). +fn inspect_flips( + triangulation: &T, + result: &mut Vec, + buffer: &mut Vec, + edge_to_validate: FixedDirectedEdgeHandle, + position: Point2<::Scalar>, +) where + T: Triangulation, +{ + buffer.clear(); + buffer.push(edge_to_validate); + + while let Some(edge) = buffer.pop() { + let edge = triangulation.directed_edge(edge); + + let v2 = edge.opposite_vertex(); + let v1 = edge.from(); + + let mut should_flip = false; + + if let Some(v2) = v2 { + let v0 = edge.to().position(); + let v1 = v1.position(); + let v3 = position; + debug_assert!(math::is_ordered_ccw(v2.position(), v1, v0)); + should_flip = math::contained_in_circumference(v2.position(), v1, v0, v3); + + if should_flip { + let e1 = edge.next().fix().rev(); + let e2 = edge.prev().fix().rev(); + + buffer.push(e1); + buffer.push(e2); + } + } + + if !should_flip { + result.push(edge.fix().rev()); + } + } +} + +fn two_point_interpolation<'a, T>( + v0: VertexHandle<'a, T::Vertex, T::DirectedEdge, T::UndirectedEdge, T::Face>, + v1: VertexHandle<'a, T::Vertex, T::DirectedEdge, T::UndirectedEdge, T::Face>, + position: Point2<::Scalar>, +) -> [::Scalar; 2] +where + T: Triangulation, + ::Scalar: Float, +{ + let projection = math::project_point(v0.position(), v1.position(), position); + let rel = projection.relative_position(); + let one: ::Scalar = 1.0.into(); + [one - rel, rel] +} + +#[cfg(test)] +mod test { + use approx::assert_ulps_eq; + + use crate::test_utilities::{random_points_in_range, random_points_with_seed, SEED, SEED2}; + use crate::{DelaunayTriangulation, HasPosition, InsertionError, Point2, Triangulation}; + use alloc::vec; + use alloc::vec::Vec; + + struct PointWithHeight { + position: Point2, + height: f64, + } + + impl PointWithHeight { + fn new(position: Point2, height: f64) -> Self { + Self { position, height } + } + } + + impl HasPosition for PointWithHeight { + type Scalar = f64; + + fn position(&self) -> Point2 { + self.position + } + } + + #[test] + fn test_natural_neighbors() -> Result<(), InsertionError> { + let vertices = random_points_with_seed(50, SEED); + let t = DelaunayTriangulation::<_>::bulk_load(vertices)?; + + let mut nn_edges = Vec::new(); + + let mut buffer = Vec::new(); + let query_point = Point2::new(0.5, 0.2); + super::get_natural_neighbor_edges(&t, &mut buffer, query_point, &mut nn_edges); + + assert!(nn_edges.len() >= 3); + + for edge in &nn_edges { + let edge = t.directed_edge(*edge); + assert!(edge.side_query(query_point).is_on_left_side()); + } + + let mut nns = Vec::new(); + let natural_neighbor = &t.natural_neighbor(); + for v in t.vertices() { + natural_neighbor.get_weights(v.position(), &mut nns); + let expected = vec![(v.fix(), 1.0f64)]; + assert_eq!(nns, expected); + } + + Ok(()) + } + + #[test] + fn test_small_interpolation() -> Result<(), InsertionError> { + let mut t = DelaunayTriangulation::<_>::new(); + // Create symmetric triangle - the weights should be mirrored + t.insert(PointWithHeight::new(Point2::new(0.0, 2.0f64.sqrt()), 0.0))?; + let v1 = t.insert(PointWithHeight::new(Point2::new(-1.0, -1.0), 0.0))?; + let v2 = t.insert(PointWithHeight::new(Point2::new(1.0, -1.0), 0.0))?; + + let mut result = Vec::new(); + t.natural_neighbor() + .get_weights(Point2::new(0.0, 0.0), &mut result); + + assert_eq!(result.len(), 3); + let mut v1_weight = None; + let mut v2_weight = None; + for (v, weight) in result { + if v == v1 { + v1_weight = Some(weight); + } + if v == v2 { + v2_weight = Some(weight); + } else { + assert!(weight > 0.0); + } + } + + assert!(v1_weight.is_some()); + assert!(v2_weight.is_some()); + assert_ulps_eq!(v1_weight.unwrap(), v2_weight.unwrap()); + + Ok(()) + } + + #[test] + fn test_quad_interpolation() -> Result<(), InsertionError> { + let mut t = DelaunayTriangulation::<_>::new(); + // Insert points into the corners of the unit square. The weights at the origin should then + // all be 0.25 + t.insert(Point2::new(1.0, 1.0))?; + t.insert(Point2::new(1.0, -1.0))?; + t.insert(Point2::new(-1.0, 1.0))?; + t.insert(Point2::new(-1.0, -1.0))?; + + let mut result = Vec::new(); + t.natural_neighbor() + .get_weights(Point2::new(0.0, 0.0), &mut result); + + assert_eq!(result.len(), 4); + for (_, weight) in result { + assert_ulps_eq!(weight, 0.25); + } + + Ok(()) + } + + #[test] + fn test_constant_height_interpolation() -> Result<(), InsertionError> { + let mut t = DelaunayTriangulation::<_>::new(); + let vertices = random_points_with_seed(50, SEED); + let fixed_height = 1.5; + for v in vertices { + t.insert(PointWithHeight::new(v, fixed_height))?; + } + + for v in random_points_in_range(1.5, 50, SEED2) { + let value = t.natural_neighbor().interpolate(|p| p.data().height, v); + if let Some(value) = value { + assert_ulps_eq!(value, 1.5); + } + } + + Ok(()) + } + + #[test] + fn test_slope_interpolation() -> Result<(), InsertionError> { + // Insert vertices v with + // v.x in [-1.0 .. 1.0] + // and + // v.height = v.x . + // + // The expected side profile of the triangulation (YZ plane through y = 0.0) looks like this: + // + // + // ↑ /⇖x = 1, height = 1 + // height / + // / + // x→ / <---- x = -1, height = -1 + let mut t = DelaunayTriangulation::<_>::new(); + let grid_size = 32; + let scale = 1.0 / grid_size as f64; + for x in -grid_size..=grid_size { + for y in -grid_size..=grid_size { + let coords = Point2::new(x as f64, y as f64).mul(scale); + t.insert(PointWithHeight::new(coords, coords.x))?; + } + } + let query_points = random_points_with_seed(50, SEED); + + let check = |point, expected| { + let value = t + .natural_neighbor() + .interpolate(|v| v.data().height, point) + .unwrap(); + assert_ulps_eq!(value, expected, epsilon = 0.001); + }; + + // Check inside points + for point in query_points { + check(point, point.x); + } + + // Check positions on the boundary + check(Point2::new(-1.0, 0.0), -1.0); + check(Point2::new(-1.0, 0.2), -1.0); + check(Point2::new(-1.0, 0.5), -1.0); + check(Point2::new(-1.0, -1.0), -1.0); + check(Point2::new(1.0, 0.0), 1.0); + check(Point2::new(1.0, 0.2), 1.0); + check(Point2::new(1.0, 0.5), 1.0); + check(Point2::new(1.0, -1.0), 1.0); + + for x in [-1.0, -0.8, -0.5, 0.0, 0.3, 0.7, 1.0] { + check(Point2::new(x, 1.0), x); + check(Point2::new(x, -1.0), x); + } + + let expect_none = |point| { + assert!(t + .natural_neighbor() + .interpolate(|v| v.data().height, point) + .is_none()); + }; + + // Check positions outside of the triangulation. + for x in [-5.0f64, -4.0, 3.0, 2.0] { + expect_none(Point2::new(x, 0.5)); + expect_none(Point2::new(x, -0.5)); + expect_none(Point2::new(x, 0.1)); + } + + Ok(()) + } + + #[test] + fn test_parabola_interpolation() -> Result<(), InsertionError> { + // Insert vertices into a regular grid and assign a height of r*r (r = distance from origin). + let mut t = DelaunayTriangulation::<_>::new(); + let grid_size = 32; + let scale = 1.0 / grid_size as f64; + for x in -grid_size..=grid_size { + for y in -grid_size..=grid_size { + let coords = Point2::new(x as f64, y as f64).mul(scale); + t.insert(PointWithHeight::new(coords, coords.length2()))?; + } + } + let query_points = random_points_with_seed(50, SEED); + + for point in query_points { + let value = t + .natural_neighbor() + .interpolate(|v| v.data().height, point) + .unwrap(); + let r2 = point.length2(); + assert_ulps_eq!(value, r2, epsilon = 0.001); + } + + Ok(()) + } +} diff --git a/src/delaunay_core/math.rs b/src/delaunay_core/math.rs index bd8a08d..4005a55 100644 --- a/src/delaunay_core/math.rs +++ b/src/delaunay_core/math.rs @@ -1,11 +1,12 @@ use crate::{HasPosition, LineSideInfo, Point2, SpadeNum}; -use num_traits::Float; +use num_traits::{zero, Float}; /// Indicates a point's projected position relative to an edge. /// /// This struct is usually the result of calling /// [DirectedEdgeHandle::project_point](crate::handles::DirectedEdgeHandle::project_point), refer to its /// documentation for more information. +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Hash)] pub struct PointProjection { factor: S, length_2: S, @@ -196,11 +197,14 @@ impl PointProjection { /// Returns the inverse of this point projection. /// - /// The inverse projection projects the same point on the *reversed* edge used by the original projection. + /// The inverse projection projects the same point on the *reversed* edge used by the original projection. + /// + /// This method can return an incorrect projection due to rounding issues if the projected point is close to one of + /// the original edge's vertices. pub fn reversed(&self) -> Self { Self { - factor: -self.factor, - length_2: -self.length_2, + factor: self.length_2 - self.factor, + length_2: self.length_2, } } } @@ -215,7 +219,13 @@ impl PointProjection { /// point lies "before" `self.from`. Analogously, a value close to 1. or greater than 1. is /// returned if the projected point is equal to or lies behind `self.to`. pub fn relative_position(&self) -> S { - self.factor / self.length_2 + if self.length_2 >= zero() { + self.factor / self.length_2 + } else { + let l = -self.length_2; + let f = -self.factor; + (l - f) / l + } } } @@ -383,6 +393,58 @@ mod test { use crate::{InsertionError, Point2}; use approx::assert_relative_eq; + #[test] + fn test_point_projection() { + use super::project_point; + + let from = Point2::new(1.0f64, 1.0); + let to = Point2::new(4.0, 5.0); + let normal = Point2::new(4.0, -3.0); + + let projection = project_point(from, to, from); + let reversed = projection.reversed(); + + assert!(!projection.is_before_edge()); + assert!(!projection.is_behind_edge()); + assert!(!reversed.is_before_edge()); + assert!(!reversed.is_behind_edge()); + + assert!(projection.is_on_edge()); + assert!(reversed.is_on_edge()); + + assert_eq!(projection.relative_position(), 0.0); + assert_eq!(reversed.relative_position(), 1.0); + + assert_eq!(projection, reversed.reversed()); + + // Create point which projects onto the mid + let mid_point = Point2::new(2.5 + normal.x, 3.0 + normal.y); + + let projection = project_point(from, to, mid_point); + assert!(projection.is_on_edge()); + assert_eq!(projection.relative_position(), 0.5); + assert_eq!(projection.reversed().relative_position(), 0.5); + + // Create point which projects onto 20% of the line + let fifth = Point2::new(0.8 * from.x + 0.2 * to.x, 0.8 * from.y + 0.2 * to.y); + let fifth = Point2::new(fifth.x + normal.x, fifth.y + normal.y); + let projection = project_point(from, to, fifth); + assert!(projection.is_on_edge()); + assert_relative_eq!(projection.relative_position(), 0.2); + assert_relative_eq!(projection.reversed().relative_position(), 0.8); + + // Check point before / behind + let behind_point = Point2::new(0.0, 0.0); + let projection = project_point(from, to, behind_point); + let reversed = projection.reversed(); + + assert!(projection.is_before_edge()); + assert!(reversed.is_behind_edge()); + + assert!(!projection.is_on_edge()); + assert!(!reversed.is_on_edge()); + } + #[test] fn test_validate_coordinate() { use super::{validate_coordinate, InsertionError::*}; diff --git a/src/delaunay_core/mod.rs b/src/delaunay_core/mod.rs index 3217313..800537b 100644 --- a/src/delaunay_core/mod.rs +++ b/src/delaunay_core/mod.rs @@ -12,6 +12,7 @@ mod triangulation_ext; pub mod refinement; +pub mod interpolation; pub mod math; pub use bulk_load::bulk_load; diff --git a/src/delaunay_core/triangulation_ext.rs b/src/delaunay_core/triangulation_ext.rs index 160facf..a63f0d2 100644 --- a/src/delaunay_core/triangulation_ext.rs +++ b/src/delaunay_core/triangulation_ext.rs @@ -714,7 +714,7 @@ pub trait TriangulationExt: Triangulation { /// For more details, refer to /// Olivier Devillers. Vertex Removal in Two Dimensional Delaunay Triangulation: /// Speed-up by Low Degrees Optimization. - /// https://doi.org/10.1016/j.comgeo.2010.10.001 + /// /// /// Note that the described low degrees optimization is not yet part of this library. fn legalize_edges_after_removal( diff --git a/src/delaunay_triangulation.rs b/src/delaunay_triangulation.rs index b547120..c062a6d 100644 --- a/src/delaunay_triangulation.rs +++ b/src/delaunay_triangulation.rs @@ -1,9 +1,11 @@ use super::delaunay_core::Dcel; use crate::{ - handles::VertexHandle, HasPosition, HintGenerator, LastUsedVertexHintGenerator, Point2, - Triangulation, TriangulationExt, + handles::VertexHandle, HasPosition, HintGenerator, LastUsedVertexHintGenerator, + NaturalNeighbor, Point2, Triangulation, TriangulationExt, }; +use num_traits::Float; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -58,6 +60,7 @@ use serde::{Deserialize, Serialize}; /// #[doc = concat!(include_str!("../images/lhs.svg"), "",include_str!("../images/rhs.svg"), " ")] /// # Extracting geometry information +/// /// Spade uses [handles](crate::handles) to extract the triangulation's geometry. /// Handles are usually retrieved by inserting a vertex or by iterating. /// @@ -284,7 +287,7 @@ where /// Returns `None` if the triangulation is empty. /// /// # Runtime - /// This method take O(sqrt(n)) on average where n is the number of vertices. + /// This method takes `O(sqrt(n))` on average where n is the number of vertices. pub fn nearest_neighbor( &self, position: Point2<::Scalar>, @@ -318,6 +321,22 @@ where } } +impl DelaunayTriangulation +where + V: HasPosition, + DE: Default, + UE: Default, + F: Default, + V::Scalar: Float, + L: HintGenerator<::Scalar>, +{ + /// Allows using natural neighbor interpolation on this triangulation. Refer to the documentation + /// of [NaturalNeighbor] for more information. + pub fn natural_neighbor(&self) -> NaturalNeighbor { + NaturalNeighbor::new(self) + } +} + impl Triangulation for DelaunayTriangulation where V: HasPosition, diff --git a/src/lib.rs b/src/lib.rs index 0740a06..1393a66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ //! * Supports vertex removal //! * Serde support with the `serde` feature. //! * `no_std` support with `default-features = false` +//! * Natural neighbor interpolation: [NaturalNeighbor] #![no_std] #![forbid(unsafe_code)] @@ -45,6 +46,7 @@ pub use delaunay_core::{ LastUsedVertexHintGenerator, RefinementParameters, RefinementResult, }; +pub use crate::delaunay_core::interpolation::{Barycentric, NaturalNeighbor}; pub use delaunay_core::LineSideInfo; pub use triangulation::{FloatTriangulation, PositionInTriangulation, Triangulation}; diff --git a/src/triangulation.rs b/src/triangulation.rs index 87872f4..63218f7 100644 --- a/src/triangulation.rs +++ b/src/triangulation.rs @@ -8,6 +8,7 @@ use crate::flood_fill_iterator::FloodFillIterator; use crate::flood_fill_iterator::RectangleMetric; use crate::flood_fill_iterator::VerticesInShapeIterator; use crate::iterators::*; +use crate::Barycentric; use crate::HintGenerator; use crate::{delaunay_core::Dcel, handles::*}; use crate::{HasPosition, InsertionError, Point2, TriangulationExt}; @@ -700,6 +701,15 @@ where VerticesInShapeIterator::new(FloodFillIterator::new(self, distance_metric, center)) } + + /// Used for barycentric interpolation on this triangulation. Refer to the documentation of + /// [Barycentric] and [crate::NaturalNeighbor] for more information. + /// + /// *Note:* In contrast to the other interpolation algorithms, barycentric interpolation also works + /// for [crate::ConstrainedDelaunayTriangulation]s. + fn barycentric(&self) -> Barycentric { + Barycentric::new(self) + } } impl FloatTriangulation for T