From ccb598ac1f8d670c3148b24541ab544030178086 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 21 Jan 2026 19:17:49 -0800 Subject: [PATCH] adds benchmark, perf parity with source crate --- Cargo.lock | 179 +++++++++++++ Cargo.toml | 8 + benches/bench_insert_bulk.rs | 26 ++ benches/bench_range_search.rs | 81 ++++++ benches/main.rs | 6 + benches/shared.rs | 34 +++ src/db.rs | 4 +- src/lib.rs | 4 +- src/spindex.rs | 470 ++-------------------------------- 9 files changed, 357 insertions(+), 455 deletions(-) create mode 100644 benches/bench_insert_bulk.rs create mode 100644 benches/bench_range_search.rs create mode 100644 benches/main.rs create mode 100644 benches/shared.rs diff --git a/Cargo.lock b/Cargo.lock index 98bd6b2..5d997ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -173,6 +182,18 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + [[package]] name = "anymap3" version = "1.0.1" @@ -334,6 +355,7 @@ name = "autobarts" version = "0.1.0" dependencies = [ "bevy", + "criterion", "dirs", "include_dir", "ordered-float", @@ -1766,6 +1788,12 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.4" @@ -1825,6 +1853,33 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1836,6 +1891,31 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + [[package]] name = "codegen" version = "0.2.0" @@ -2111,6 +2191,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" +dependencies = [ + "cast", + "itertools 0.13.0", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -2126,6 +2240,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -4009,6 +4142,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "option-ext" version = "0.2.0" @@ -4043,6 +4182,16 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking" version = "2.2.1" @@ -4369,6 +4518,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "read-fonts" version = "0.35.0" @@ -5109,6 +5278,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index c6f0076..d0e6be4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] bevy = { version = "0.18", default-features = false, features = ["2d"] } +criterion = { version = "0.8.1", default-features = false, features = ["cargo_bench_support", "rayon"] } dirs = "6.0.0" include_dir = "0.7.4" ordered-float = "5.1.0" @@ -19,3 +20,10 @@ opt-level = 1 # Enable a large amount of optimization in the dev profile for dependencies. [profile.dev.package."*"] opt-level = 3 + +[profile.bench] +inherits = "release" + +[[bench]] +name = "main" +harness = false diff --git a/benches/bench_insert_bulk.rs b/benches/bench_insert_bulk.rs new file mode 100644 index 0000000..93fd97c --- /dev/null +++ b/benches/bench_insert_bulk.rs @@ -0,0 +1,26 @@ +#[path = "shared.rs"] +mod shared; +use std::hint::black_box; + +use autobarts::spindex::RStarTree; +use criterion::{Criterion, criterion_group}; +use shared::*; + +fn bench_insert_bulk_rstartree_2d(_c: &mut Criterion) { + let points = generate_2d_data(); + let mut cc = configure_criterion(); + cc.bench_function("insert_bulk_2d_rstartree", |b| { + b.iter_with_setup( + || { + let tree = RStarTree::new(BENCH_NODE_CAPACITY); + (tree, points.clone()) + }, + |(mut tree, points)| { + tree.insert_bulk(points); + black_box(()); + }, + ) + }); +} + +criterion_group!(benches, bench_insert_bulk_rstartree_2d,); diff --git a/benches/bench_range_search.rs b/benches/bench_range_search.rs new file mode 100644 index 0000000..8dfabec --- /dev/null +++ b/benches/bench_range_search.rs @@ -0,0 +1,81 @@ +#[path = "shared.rs"] +mod shared; +use std::hint::black_box; + +use autobarts::{geom::Point, spindex::RStarTree}; +use bevy::math::Vec2; +use criterion::{Criterion, criterion_group}; +use shared::*; + +const BENCH_RANGE_RADIUS: f32 = 30.0; + +// Configure Criterion with our benchmark timeout. +pub fn configure_criterion() -> Criterion { + Criterion::default().measurement_time(BENCH_TIMEOUT) +} + +/// A generic helper function for range search benchmarks. +/// +/// The lifetime `'a` ties the lifetime of the tree reference and the return +/// value. The closure `search_fn` must return a value whose lifetime is at +/// least `'a`. +fn bench_range_search<'a, T, Q, R>( + name: &str, + tree: &'a T, + query: &Q, + search_fn: impl Fn(&'a T, &Q, f32) -> R, + cc: &mut Criterion, +) where + R: 'a, +{ + cc.bench_function(name, |b| { + b.iter(|| { + let res = search_fn(tree, query, BENCH_RANGE_RADIUS); + black_box(res) + }) + }); +} + +fn benchmark_range_rstartree_2d(_c: &mut Criterion) { + let points = generate_2d_data(); + let mut tree = RStarTree::new(BENCH_NODE_CAPACITY); + + tree.insert_bulk(points.clone()); + + let mut cc = configure_criterion(); + let mut idx = 0; + let len = points.len(); + cc.bench_function("range_rstartree_2d", |b| { + b.iter(|| { + let res = tree.range_search(&points[idx], BENCH_RANGE_RADIUS); + idx = (idx + 1) % len; + black_box(res) + }) + }); +} + +fn benchmark_range_bbox_rstartree_2d(_c: &mut Criterion) { + let points = generate_2d_data(); + let mut tree = RStarTree::new(BENCH_NODE_CAPACITY); + + tree.insert_bulk(points); + + let query_rect = bevy::math::bounding::Aabb2d::new( + Vec2::new(35.0 - BENCH_RANGE_RADIUS, 45.0 - BENCH_RANGE_RADIUS), + 4.0 * Vec2::splat(BENCH_RANGE_RADIUS), + ); + let mut cc = configure_criterion(); + bench_range_search( + "range_rstartree_bbox_2d", + &tree, + &query_rect, + |t, q, _| t.range_search_bbox(q), + &mut cc, + ); +} + +criterion_group!( + benches, + benchmark_range_rstartree_2d, + //benchmark_range_bbox_rstartree_2d, +); diff --git a/benches/main.rs b/benches/main.rs new file mode 100644 index 0000000..a2cb141 --- /dev/null +++ b/benches/main.rs @@ -0,0 +1,6 @@ +use criterion::criterion_main; + +mod bench_insert_bulk; +mod bench_range_search; + +criterion_main!(bench_insert_bulk::benches, bench_range_search::benches,); diff --git a/benches/shared.rs b/benches/shared.rs new file mode 100644 index 0000000..520e784 --- /dev/null +++ b/benches/shared.rs @@ -0,0 +1,34 @@ +use autobarts::geom::Point; +use criterion::Criterion; + +// +// Benchmark Parameters +// +pub const BENCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); +pub const BENCH_NUM_INSERT: i32 = 50_000; +pub const BENCH_NODE_CAPACITY: usize = 5; + +// +// Data Generation Functions (Raw Data) +// +pub fn generate_2d_data() -> Vec { + let data: Vec = (0..BENCH_NUM_INSERT) + .map(|i| { + let point = bevy::prelude::Vec2::new(i as f32, i as f32); + + Point { + point, + entity: bevy::prelude::Entity::PLACEHOLDER, + } + }) + .collect(); + + data +} + +// Configure Criterion with a timeout for benchmarks +pub fn configure_criterion() -> Criterion { + Criterion::default() + .measurement_time(BENCH_TIMEOUT) + .sample_size(10) +} diff --git a/src/db.rs b/src/db.rs index 2e343c4..cae13cc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, sync::LazyLock}; -use include_dir::{include_dir, Dir}; -use rusqlite::{config::DbConfig, Connection}; +use include_dir::{Dir, include_dir}; +use rusqlite::{Connection, config::DbConfig}; use rusqlite_migration::Migrations; static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); diff --git a/src/lib.rs b/src/lib.rs index e5e5eaf..0cf32e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ pub mod db; pub mod geom; -mod spindex; - -pub use spindex::*; +pub mod spindex; diff --git a/src/spindex.rs b/src/spindex.rs index 2d5ef6f..ab37f5b 100644 --- a/src/spindex.rs +++ b/src/spindex.rs @@ -1,5 +1,3 @@ -use std::cmp::Ordering; - use bevy::{ math::bounding::{Aabb2d, BoundingVolume, IntersectsVolume}, prelude::*, @@ -7,9 +5,6 @@ use bevy::{ use crate::geom::Point; -// Epsilon value for zero-sizes bounding boxes/cubes. -const EPSILON: f32 = 1e-10; - /// An entry in the R*‑tree, which can be either a leaf or a node. #[derive(Debug, Clone)] @@ -60,44 +55,24 @@ impl Entry { _ => None, } } - fn child_mut(&mut self) -> Option<&mut TreeNode> { - match self { - Entry::Node { child, .. } => Some(child), - _ => None, - } - } - fn set_mbr(&mut self, new_mbr: Aabb2d) { - if let Entry::Node { mbr, .. } = self { - *mbr = new_mbr; - } - } - fn into_child(self) -> Option { - match self { - Entry::Node { child, .. } => Some(child), - _ => None, - } - } } impl TreeNode { fn is_leaf(&self) -> bool { self.is_leaf } + fn entries(&self) -> &[Entry] { &self.entries } - fn entries_mut(&mut self) -> &mut [Entry] { - &mut self.entries - } - fn range_search_bbox(&self, bbox: &Aabb2d) -> Vec<&Point> { - let mut result = Vec::new(); + fn range_search_bbox<'s>(&'s self, bbox: &Aabb2d, out: &mut Vec<&'s Point>) { if self.is_leaf() { for entry in self.entries() { if let Some(obj) = entry.as_leaf_obj() && entry.mbr().intersects(bbox) { - result.push(obj); + out.push(obj); } } } else { @@ -105,11 +80,10 @@ impl TreeNode { if let Some(child) = entry.child() && entry.mbr().intersects(bbox) { - result.extend_from_slice(&child.range_search_bbox(bbox)); + child.range_search_bbox(bbox, out); } } } - result } fn mbr(&self) -> Option { @@ -129,99 +103,26 @@ impl RStarTree { } } - /// Inserts an object into the R*‑tree. - /// - /// # Arguments - /// - /// * `object` - The object to insert. - pub fn insert(&mut self, object: Point) { - let entry = Entry::Leaf { - mbr: object.mbr(), - object, - }; - self.insert_entry(entry, None); + pub fn range_search(&self, query_point: &Point, radius: f32) -> Vec<&Point> { + let query_bbox = Aabb2d::new(query_point.point, Vec2::splat(radius)); + let r2 = radius * radius; + let candidates = self.range_search_bbox(&query_bbox); + candidates + .into_iter() + .filter(|&other| { + ((query_point.point.x - other.point.x) + (query_point.point.y - other.point.y)) + .powi(2) + <= r2 + }) + .collect() } - fn insert_entry(&mut self, entry: Entry, mut reinsert_level: Option) { - let mut to_insert = vec![(entry, 0)]; - while let Some((item, level)) = to_insert.pop() { - let overflow = insert_recursive( - &mut self.root, - item, - self.max_entries, - level, - &mut reinsert_level, - &mut to_insert, - ); - - if let Some((overflowed_node, overflow_level)) = overflow { - if reinsert_level == Some(overflow_level) { - let old_entries = overflowed_node; - let (group1, group2) = split_entries(old_entries, self.max_entries); - let mut mbr1 = *group1[0].mbr(); - for entry in group1.iter() { - mbr1 = mbr1.merge(entry.mbr()); - } - let mut mbr2 = *group2[0].mbr(); - for entry in group2.iter() { - mbr2 = mbr2.merge(entry.mbr()); - } - let child1 = TreeNode { - entries: group1, - is_leaf: self.root.is_leaf, - }; - let child2 = TreeNode { - entries: group2, - is_leaf: self.root.is_leaf, - }; - - self.root.is_leaf = false; - self.root.entries.clear(); - self.root.entries.push(Entry::Node { - mbr: mbr1, - child: child1, - }); - self.root.entries.push(Entry::Node { - mbr: mbr2, - child: child2, - }); - } else { - if reinsert_level.is_none() { - reinsert_level = Some(overflow_level); - } - let mut node = TreeNode { - entries: overflowed_node, - is_leaf: self.root.is_leaf, - }; - let reinserted_entries = forced_reinsert(&mut node, self.max_entries); - self.root.entries = node.entries; - for entry in reinserted_entries { - to_insert.push((entry, 0)); - } - } - } - } - } - - /// Performs a range search with a given query bounding volume. - /// - /// # Arguments - /// - /// * `query` - The bounding volume to search against. - /// - /// # Returns - /// - /// A vector of references to the objects whose minimum bounding volumes - /// intersect the query. pub fn range_search_bbox(&self, query_bbox: &Aabb2d) -> Vec<&Point> { - self.root.range_search_bbox(query_bbox) + let mut res = Vec::with_capacity(self.root.entries.len() / 10); + self.root.range_search_bbox(query_bbox, &mut res); + res } - /// Inserts a bulk of objects into the R*-tree. - /// - /// # Arguments - /// - /// * `objects` - The objects to insert. pub fn insert_bulk(&mut self, objects: Vec) { if objects.is_empty() { return; @@ -254,343 +155,12 @@ impl RStarTree { self.root.entries.extend(entries); } - - #[doc(hidden)] - pub fn height(&self) -> usize { - let mut height = 1; - let mut current_node = &self.root; - while !current_node.is_leaf { - height += 1; - current_node = if let Some(Entry::Node { child, .. }) = current_node.entries.first() { - child - } else { - break; - }; - } - height - } } -fn choose_subtree(node: &TreeNode, entry: &Entry) -> usize { - let children_are_leaves = if let Some(Entry::Node { child, .. }) = node.entries.first() { - child.is_leaf - } else { - false - }; - - if children_are_leaves { - node.entries - .iter() - .enumerate() - .min_by(|&(_, a), &(_, b)| { - let mbr_a = a.mbr(); - let mbr_b = b.mbr(); - - let overlap_a = node - .entries - .iter() - .filter(|e| !std::ptr::eq(*e, a)) - .map(|e| e.mbr().merge(entry.mbr()).overlap(e.mbr())) - .sum::(); - - let overlap_b = node - .entries - .iter() - .filter(|e| !std::ptr::eq(*e, b)) - .map(|e| e.mbr().merge(entry.mbr()).overlap(e.mbr())) - .sum::(); - - let overlap_cmp = overlap_a.partial_cmp(&overlap_b).unwrap_or(Ordering::Equal); - if overlap_cmp != Ordering::Equal { - return overlap_cmp; - } - - let enlargement_a = mbr_a.merge(entry.mbr()).visible_area() - mbr_a.visible_area(); - let enlargement_b = mbr_b.merge(entry.mbr()).visible_area() - mbr_b.visible_area(); - let enlargement_cmp = enlargement_a - .partial_cmp(&enlargement_b) - .unwrap_or(Ordering::Equal); - if enlargement_cmp != Ordering::Equal { - return enlargement_cmp; - } - - mbr_a - .visible_area() - .partial_cmp(&mbr_b.visible_area()) - .unwrap_or(Ordering::Equal) - }) - .map(|(i, _)| i) - .unwrap_or(0) - } else { - node.entries - .iter() - .enumerate() - .min_by(|(_, a), (_, b)| { - let mbr_a = a.mbr(); - let mbr_b = b.mbr(); - - let enlargement_a = mbr_a.merge(entry.mbr()).visible_area() - mbr_a.visible_area(); - let enlargement_b = mbr_b.merge(entry.mbr()).visible_area() - mbr_b.visible_area(); - - let enlargement_cmp = enlargement_a - .partial_cmp(&enlargement_b) - .unwrap_or(Ordering::Equal); - if enlargement_cmp != Ordering::Equal { - return enlargement_cmp; - } - mbr_a - .visible_area() - .partial_cmp(&mbr_b.visible_area()) - .unwrap_or(Ordering::Equal) - }) - .map(|(i, _)| i) - .unwrap_or(0) - } -} - -fn insert_recursive( - node: &mut TreeNode, - entry: Entry, - max_entries: usize, - level: usize, - reinsert_level: &mut Option, - to_insert_queue: &mut Vec<(Entry, usize)>, -) -> Option<(Vec, usize)> { - if node.is_leaf { - node.entries.push(entry); - } else { - let best_index = choose_subtree(node, &entry); - let child = if let Entry::Node { child, .. } = &mut node.entries[best_index] { - child - } else { - unreachable!() - }; - - if let Some((overflow, overflow_level)) = insert_recursive( - child, - entry, - max_entries, - level + 1, - reinsert_level, - to_insert_queue, - ) { - if reinsert_level.is_some() && *reinsert_level == Some(overflow_level) { - let (g1, g2) = split_entries(overflow, max_entries); - let mbr1 = entries_mbr(&g1) - .unwrap_or_else(|| unreachable!("non-empty group must have MBR")); - let mbr2 = entries_mbr(&g2) - .unwrap_or_else(|| unreachable!("non-empty group must have MBR")); - - let child1 = TreeNode { - entries: g1, - is_leaf: child.is_leaf, - }; - let child2 = TreeNode { - entries: g2, - is_leaf: child.is_leaf, - }; - node.entries[best_index] = Entry::Node { - mbr: mbr1, - child: child1, - }; - node.entries.push(Entry::Node { - mbr: mbr2, - child: child2, - }); - } else { - if reinsert_level.is_none() { - *reinsert_level = Some(overflow_level); - } - let mut overflowed_node = TreeNode { - entries: overflow, - is_leaf: child.is_leaf, - }; - let reinserted = forced_reinsert(&mut overflowed_node, max_entries); - for item in reinserted { - to_insert_queue.push((item, 0)); - } - if let Entry::Node { child, .. } = &mut node.entries[best_index] { - child.entries = overflowed_node.entries; - } - } - } - let children = &mut node.entries_mut()[best_index]; - let Entry::Node { - child: children, - mbr, - } = children - else { - return None; - }; - - if let Some(new_mbr) = entries_mbr(children.entries()) { - *mbr = new_mbr; - } - } - - if node.entries.len() > max_entries { - return Some((std::mem::take(&mut node.entries), level)); - } - None -} - -fn forced_reinsert(node: &mut TreeNode, max_entries: usize) -> Vec { - let node_mbr = if let Some(mbr) = entries_mbr(&node.entries) { - mbr - } else { - return Vec::new(); - }; - let reinsert_count = (max_entries as f32 * 0.3).ceil() as usize; - - node.entries.sort_by(|a, b| { - let center_a: Vec = (0..2).map(|d| a.mbr().center()[d]).collect(); - let center_b: Vec = (0..2).map(|d| b.mbr().center()[d]).collect(); - let node_center: Vec = (0..2).map(|d| node_mbr.center()[d]).collect(); - - let dist_a = center_a - .iter() - .zip(node_center.iter()) - .map(|(ca, cb)| (ca - cb).powi(2)) - .sum::(); - let dist_b = center_b - .iter() - .zip(node_center.iter()) - .map(|(ca, cb)| (ca - cb).powi(2)) - .sum::(); - - dist_b.partial_cmp(&dist_a).unwrap_or(Ordering::Equal) - }); - - node.entries.drain(0..reinsert_count).collect() -} - -fn split_entries(mut entries: Vec, max_entries: usize) -> (Vec, Vec) { - let min_entries = (max_entries as f32 * 0.4).ceil() as usize; - let mut best_axis = 0; - let mut best_split_index = 0; - let mut min_margin = f32::INFINITY; - - for dim in 0..2 { - entries.sort_by(|a, b| { - let ca = a.mbr().center()[dim]; - - let cb = b.mbr().center()[dim]; - ca.partial_cmp(&cb).unwrap_or(Ordering::Equal) - }); - - for k in min_entries..=entries.len() - min_entries { - let group1 = &entries[..k]; - let group2 = &entries[k..]; - let mut mbr1 = *group1[0].mbr(); - for entry in group1 { - mbr1 = mbr1.merge(entry.mbr()); - } - let mbr2 = *group2[0].mbr(); - - let margin = mbr1.margin() + mbr2.margin(); - if margin < min_margin { - min_margin = margin; - best_axis = dim; - best_split_index = k; - } - } - } - - entries.sort_by(|a, b| { - let ca = a.mbr().center()[best_axis]; - let cb = b.mbr().center()[best_axis]; - ca.partial_cmp(&cb).unwrap_or(Ordering::Equal) - }); - - let mut best_overlap = f32::INFINITY; - let mut best_area = f32::INFINITY; - - for k in min_entries..=entries.len() - min_entries { - let group1 = &entries[..k]; - let group2 = &entries[k..]; - let mbr1 = - entries_mbr(group1).unwrap_or_else(|| unreachable!("non-empty group must have MBR")); - let mbr2 = - entries_mbr(group2).unwrap_or_else(|| unreachable!("non-empty group must have MBR")); - let overlap = mbr1.overlap(&mbr2); - let area = mbr1.visible_area() + mbr2.visible_area(); - - if overlap < best_overlap { - best_overlap = overlap; - best_area = area; - best_split_index = k; - } else if (overlap - best_overlap).abs() < EPSILON && area < best_area { - best_area = area; - best_split_index = k; - } - } - - let (group1, group2) = entries.split_at(best_split_index); - (group1.to_vec(), group2.to_vec()) -} - -impl RStarTree { - /// Performs a range search on the R*‑tree using a query object and radius. - /// - /// The query object is wrapped into a bounding volume using - /// `from_point_radius`. - /// - /// # Arguments - /// - /// * `query` - The query object. - /// * `radius` - The search radius. - /// - /// # Returns - /// - /// A vector of references to the objects within the given radius. - pub fn range_search(&self, query_point: &Point, radius: f32) -> Vec<&Point> { - let query_bbox = Aabb2d::new(query_point.point, Vec2::splat(radius)); - let r2 = radius.powi(2); - let candidates = self.range_search_bbox(&query_bbox); - candidates - .into_iter() - .filter(|other| query_point.point.distance_squared(other.point) <= r2) - .collect() - } -} +impl RStarTree {} fn entries_mbr(entries: &[Entry]) -> Option { let mut iter = entries.iter(); let first = *iter.next()?.mbr(); Some(iter.fold(first, |acc, entry| acc.merge(entry.mbr()))) } - -trait Bvr: BoundingVolume { - fn margin(&self) -> f32; - - fn overlap(&self, other: &Self) -> f32; -} - -impl Bvr for Aabb2d { - fn margin(&self) -> f32 { - let Vec2 { x, y } = self.half_size(); - 2.0 * x * y - } - - fn overlap(&self, other: &Aabb2d) -> f32 { - let Vec2 { - x: self_width, - y: self_height, - } = 2.0 * self.half_size(); - - let self_center = self.center(); - let other_center = other.center(); - - let Vec2 { - x: other_width, - y: other_height, - } = 2.0 * other.half_size(); - - let o_x = (self_center.x + self_width).min(other_center.x + other_width) - - self_center.x.max(other_center.x); - let o_y = (self_center.y + self_height).min(other_center.y + other_height) - - self_center.y.max(other_center.y); - - (o_x * o_y).max(0.0) - } -}