use std::collections::HashMap;
use bevy::asset::{AssetEvent, Assets};
use bevy::ecs::prelude::*;
use bevy::math::{IVec2, Vec2, Vec3, Vec3Swizzles};
use bevy::shape::Ray3d;
use bevy::transform::components::GlobalTransform;
use crate::felling::{Felled, TreeLog, TreeStump};
use crate::shape::{TreeShape, WoodCapsule};
use crate::growth::fraction;
use crate::plugin::{ProceduralTree, TreeAge, TreeRender};
use crate::pool::{PooledTree, TreePool};
use crate::skeleton::Node;
#[derive(Resource, Default, Debug)]
pub struct FloraIndex {
cells: HashMap<IVec2, Vec<Entity>>,
trees: HashMap<Entity, (IndexedTree, IVec2, IVec2)>,
obstacle_cells: HashMap<IVec2, Vec<Entity>>,
obstacles: HashMap<Entity, (Vec<Obstacle>, IVec2, IVec2)>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IndexedTree {
pub entity: Entity,
pub base: Vec3,
pub axis: Vec3,
pub height: f32,
pub base_radius: f32,
pub top_radius: f32,
pub undergrowth: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TreeHit {
pub tree: Entity,
pub point: Vec3,
pub normal: Vec3,
pub distance: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Obstacle {
pub entity: Entity,
pub kind: ObstacleKind,
pub capsule: WoodCapsule,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ObstacleKind {
Stump,
Log,
Lying,
}
impl Obstacle {
pub fn distance_to(&self, at: Vec2) -> f32 {
let (a, b) = (self.capsule.a.xz(), self.capsule.b.xz());
let ab = b - a;
let t = if ab.length_squared() > 0.0 { ((at - a).dot(ab) / ab.length_squared()).clamp(0.0, 1.0) } else { 0.0 };
(at.distance(a + ab * t) - self.capsule.radius).max(0.0)
}
}
#[derive(Component, Clone, Copy, Debug)]
pub(crate) struct LyingWood {
pub(crate) cut_y: f32,
}
impl IndexedTree {
pub fn top(&self) -> Vec3 {
self.base + self.axis * self.height
}
pub fn radius_at(&self, h: f32) -> f32 {
let t = if self.height > 0.0 { (h / self.height).clamp(0.0, 1.0) } else { 0.0 };
self.base_radius + (self.top_radius - self.base_radius) * t
}
pub fn distance_to(&self, at: Vec2) -> f32 {
(self.base.xz().distance(at) - self.base_radius).max(0.0)
}
pub fn cast(&self, ray: Ray3d, max: f32) -> Option<TreeHit> {
if self.height <= 0.0 {
return None;
}
let (o, d, a) = (ray.origin, *ray.direction, self.axis);
let k = (self.top_radius - self.base_radius) / self.height;
let w = o - self.base;
let (wa, da) = (w.dot(a), d.dot(a));
let (wp, dp) = (w - a * wa, d - a * da);
let r0 = self.base_radius + k * wa;
let qa = dp.dot(dp) - k * k * da * da;
let qb = 2.0 * (wp.dot(dp) - k * da * r0);
let qc = wp.dot(wp) - r0 * r0;
let mut roots = [f32::NAN; 2];
if qa.abs() < 1.0e-9 {
if qb.abs() > 1.0e-9 {
roots[0] = -qc / qb;
}
} else {
let disc = qb * qb - 4.0 * qa * qc;
if disc < 0.0 {
return None;
}
let s = disc.sqrt();
roots = [(-qb - s) / (2.0 * qa), (-qb + s) / (2.0 * qa)];
if roots[0] > roots[1] {
roots.swap(0, 1);
}
}
roots.into_iter().filter(|t| t.is_finite() && *t >= 0.0 && *t <= max).find_map(|t| {
let h = wa + t * da;
let radius = r0 + k * da * t;
if !(0.0..=self.height).contains(&h) || radius <= 0.0 {
return None;
}
let point = o + d * t;
let out = (point - self.base - a * h).normalize_or(Vec3::X);
Some(TreeHit { tree: self.entity, point, normal: (out - a * k).normalize(), distance: t })
})
}
fn cells(&self) -> (IVec2, IVec2) {
let (top, reach) = (self.top(), self.base_radius.max(self.top_radius));
let low = self.base.xz().min(top.xz()) - Vec2::splat(reach);
let high = self.base.xz().max(top.xz()) + Vec2::splat(reach);
(cell_of(low), cell_of(high))
}
}
fn in_order(a: (f32, f32), first: Entity, b: (f32, f32), second: Entity) -> bool {
a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)).then(first.to_bits().cmp(&second.to_bits())).is_lt()
}
fn cell_of(at: Vec2) -> IVec2 {
(at / FloraIndex::CELL).floor().as_ivec2()
}
impl FloraIndex {
pub const CELL: f32 = 8.0;
pub fn len(&self) -> usize {
self.trees.len()
}
pub fn is_empty(&self) -> bool {
self.trees.is_empty()
}
pub fn get(&self, entity: Entity) -> Option<&IndexedTree> {
self.trees.get(&entity).map(|(tree, ..)| tree)
}
pub fn iter(&self) -> impl Iterator<Item = &IndexedTree> {
self.trees.values().map(|(tree, ..)| tree)
}
pub fn within(&self, at: Vec2, radius: f32) -> impl Iterator<Item = &IndexedTree> {
let (low, high) = (cell_of(at - Vec2::splat(radius)), cell_of(at + Vec2::splat(radius)));
(low.y..=high.y)
.flat_map(move |y| (low.x..=high.x).map(move |x| IVec2::new(x, y)))
.flat_map(move |cell| {
self.cells.get(&cell).into_iter().flatten().filter_map(move |entity| {
let (tree, first, _) = self.trees.get(entity)?;
(first.max(low) == cell && tree.distance_to(at) <= radius).then_some(tree)
})
})
}
pub fn nearest(&self, at: Vec2, max: f32) -> Option<&IndexedTree> {
self.within(at, max).min_by(|a, b| a.distance_to(at).total_cmp(&b.distance_to(at)))
}
pub fn cast_ray(&self, ray: Ray3d, max: f32) -> Option<TreeHit> {
let mut best: Option<TreeHit> = None;
let consider = |cell: IVec2, best: &mut Option<TreeHit>| {
for entity in self.cells.get(&cell).into_iter().flatten() {
let Some((tree, ..)) = self.trees.get(entity) else { continue };
let limit = best.map_or(max, |hit| hit.distance);
if let Some(hit) = tree.cast(ray, limit) {
*best = Some(hit);
}
}
};
let flat = Vec2::new(ray.direction.x, ray.direction.z);
let start = ray.origin.xz();
let mut cell = cell_of(start);
let speed = flat.length();
if speed < 1.0e-6 {
consider(cell, &mut best);
return best;
}
let step = IVec2::new(flat.x.signum() as i32, flat.y.signum() as i32);
let next_edge = |c: i32, s: i32| (c + i32::from(s > 0)) as f32 * Self::CELL;
let along = |edge: f32, from: f32, rate: f32| if rate.abs() < 1.0e-9 { f32::INFINITY } else { (edge - from) / rate };
let mut t_next = Vec2::new(
along(next_edge(cell.x, step.x), start.x, flat.x),
along(next_edge(cell.y, step.y), start.y, flat.y),
);
let t_step = Vec2::new(
if flat.x.abs() < 1.0e-9 { f32::INFINITY } else { Self::CELL / flat.x.abs() },
if flat.y.abs() < 1.0e-9 { f32::INFINITY } else { Self::CELL / flat.y.abs() },
);
loop {
consider(cell, &mut best);
let leave = t_next.x.min(t_next.y);
if leave > max || best.is_some_and(|hit| hit.distance <= leave) {
return best;
}
if t_next.x < t_next.y {
cell.x += step.x;
t_next.x += t_step.x;
} else {
cell.y += step.y;
t_next.y += t_step.y;
}
}
}
fn insert(&mut self, tree: IndexedTree) {
self.remove(tree.entity);
let (low, high) = tree.cells();
let Self { cells, trees, .. } = self;
let place = |e: &Entity| trees.get(e).map_or((0.0, 0.0), |(t, ..)| (t.base.x, t.base.z));
for y in low.y..=high.y {
for x in low.x..=high.x {
let list = cells.entry(IVec2::new(x, y)).or_default();
let at = list.partition_point(|e| in_order(place(e), *e, (tree.base.x, tree.base.z), tree.entity));
list.insert(at, tree.entity);
}
}
self.trees.insert(tree.entity, (tree, low, high));
}
fn remove(&mut self, entity: Entity) {
let Some((_, low, high)) = self.trees.remove(&entity) else { return };
for y in low.y..=high.y {
for x in low.x..=high.x {
let cell = IVec2::new(x, y);
if let Some(list) = self.cells.get_mut(&cell) {
list.retain(|e| *e != entity);
if list.is_empty() {
self.cells.remove(&cell);
}
}
}
}
}
pub fn obstacles_within(&self, at: Vec2, radius: f32) -> impl Iterator<Item = &Obstacle> {
let (low, high) = (cell_of(at - Vec2::splat(radius)), cell_of(at + Vec2::splat(radius)));
(low.y..=high.y)
.flat_map(move |y| (low.x..=high.x).map(move |x| IVec2::new(x, y)))
.flat_map(move |cell| {
self.obstacle_cells.get(&cell).into_iter().flatten().flat_map(move |entity| {
let found = self.obstacles.get(entity).filter(|(_, first, _)| first.max(low) == cell);
found.into_iter().flat_map(move |(wood, ..)| wood.iter().filter(move |o| o.distance_to(at) <= radius))
})
})
}
pub fn obstacles(&self) -> impl Iterator<Item = &Obstacle> {
self.obstacles.values().flat_map(|(wood, ..)| wood)
}
fn insert_obstacles(&mut self, entity: Entity, wood: Vec<Obstacle>) {
self.remove_obstacles(entity);
if wood.is_empty() {
return;
}
let (mut lo, mut hi) = (Vec2::splat(f32::INFINITY), Vec2::splat(f32::NEG_INFINITY));
for capsule in wood.iter().map(|o| o.capsule) {
for p in [capsule.a.xz(), capsule.b.xz()] {
lo = lo.min(p - Vec2::splat(capsule.radius));
hi = hi.max(p + Vec2::splat(capsule.radius));
}
}
let (low, high) = (cell_of(lo), cell_of(hi));
for y in low.y..=high.y {
for x in low.x..=high.x {
let list = self.obstacle_cells.entry(IVec2::new(x, y)).or_default();
let place = |e: &Entity| self.obstacles.get(e).map_or((0.0, 0.0), |(wood, ..)| (wood[0].capsule.a.x, wood[0].capsule.a.z));
let here = (wood[0].capsule.a.x, wood[0].capsule.a.z);
let at = list.partition_point(|e| in_order(place(e), *e, here, entity));
list.insert(at, entity);
}
}
self.obstacles.insert(entity, (wood, low, high));
}
fn remove_obstacles(&mut self, entity: Entity) {
let Some((_, low, high)) = self.obstacles.remove(&entity) else { return };
for y in low.y..=high.y {
for x in low.x..=high.x {
let cell = IVec2::new(x, y);
if let Some(list) = self.obstacle_cells.get_mut(&cell) {
list.retain(|e| *e != entity);
if list.is_empty() {
self.obstacle_cells.remove(&cell);
}
}
}
}
}
}
struct Trunk {
start: Vec3,
direction: Vec3,
length: f32,
base_radius: f32,
tip_radius: f32,
}
impl Trunk {
fn grown(node: &Node, progress: f32, radius_progress: f32) -> Self {
Trunk {
start: node.start,
direction: node.direction,
length: node.length * progress,
base_radius: node.base_radius * radius_progress,
tip_radius: node.tip_radius * radius_progress,
}
}
fn placed(&self, entity: Entity, place: &GlobalTransform, undergrowth: bool) -> IndexedTree {
let base = place.transform_point(self.start);
let top = place.transform_point(self.start + self.direction * self.length);
let height = base.distance(top);
let scale = place.affine().matrix3.x_axis.length();
IndexedTree {
entity,
base,
axis: if height > 0.0 { (top - base) / height } else { place.up().as_vec3() },
height,
base_radius: self.base_radius * scale,
top_radius: self.tip_radius * scale,
undergrowth,
}
}
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub(crate) fn index_trees(
mut index: ResMut<FloraIndex>,
pools: Option<Res<Assets<TreePool>>>,
mut pool_events: MessageReader<AssetEvent<TreePool>>,
mut gone_procedural: RemovedComponents<ProceduralTree>,
mut gone_pooled: RemovedComponents<PooledTree>,
felled: Query<Entity, Added<Felled>>,
procedural: Query<
(Entity, &ProceduralTree, &TreeAge, Option<&TreeRender>, &GlobalTransform),
(
Without<Felled>,
Or<(Changed<GlobalTransform>, Changed<ProceduralTree>, Changed<TreeAge>, Changed<TreeRender>)>,
),
>,
moved_pooled: Query<(Entity, &PooledTree, &GlobalTransform), (Without<Felled>, Or<(Changed<GlobalTransform>, Changed<PooledTree>)>)>,
all_pooled: Query<(Entity, &PooledTree, &GlobalTransform), Without<Felled>>,
) {
for entity in gone_procedural.read().chain(gone_pooled.read()).chain(&felled) {
index.remove(entity);
}
for (entity, tree, age, render, place) in &procedural {
let shape = match render {
Some(render) => {
let node = &render.full.nodes[0];
let progress = fraction(age.0, node.birth, node.duration);
let thickening = fraction(age.0, node.birth, node.duration * tree.0.growth.thickening_lag);
Trunk::grown(node, progress, thickening)
}
None => {
let params = &tree.0.trunk;
Trunk {
start: Vec3::ZERO,
direction: Vec3::Y,
length: params.height,
base_radius: params.radius * params.flare,
tip_radius: params.radius * params.taper,
}
}
};
index.insert(shape.placed(entity, place, tree.0.undergrowth));
}
let Some(pools) = pools else { return };
let mut reloaded = Vec::new();
for event in pool_events.read() {
if let AssetEvent::Added { id } | AssetEvent::Modified { id } | AssetEvent::LoadedWithDependencies { id } = event {
reloaded.push(*id);
}
}
let mut add = |entity: Entity, pooled: &PooledTree, place: &GlobalTransform| {
let Some(pool) = pools.get(&pooled.pool) else { return };
let variant = &pool.variants[pooled.variant as usize % pool.variants.len()];
index.insert(Trunk::grown(&variant.skeleton.nodes[0], 1.0, 1.0).placed(entity, place, pool.params.undergrowth));
};
for (entity, pooled, place) in &moved_pooled {
add(entity, pooled, place);
}
if !reloaded.is_empty() {
for (entity, pooled, place) in &all_pooled {
if reloaded.contains(&pooled.pool.id()) {
add(entity, pooled, place);
}
}
}
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub(crate) fn index_obstacles(
mut index: ResMut<FloraIndex>,
mut gone_stumps: RemovedComponents<TreeStump>,
mut gone_logs: RemovedComponents<TreeLog>,
mut gone_felled: RemovedComponents<Felled>,
mut gone_lying: RemovedComponents<LyingWood>,
stumps: Query<(Entity, &TreeStump, &GlobalTransform), Or<(Changed<TreeStump>, Changed<GlobalTransform>)>>,
logs: Query<(Entity, &TreeLog, &GlobalTransform), Or<(Changed<TreeLog>, Changed<GlobalTransform>)>>,
felled: Query<(Entity, &Felled, &TreeShape, &GlobalTransform), Or<(Changed<Felled>, Changed<TreeShape>, Changed<GlobalTransform>)>>,
lying: Query<(Entity, &LyingWood, &TreeShape, &GlobalTransform), Or<(Changed<LyingWood>, Changed<TreeShape>, Changed<GlobalTransform>)>>,
) {
for entity in gone_stumps.read().chain(gone_logs.read()).chain(gone_felled.read()).chain(gone_lying.read()) {
index.remove_obstacles(entity);
}
for (entity, stump, place) in &stumps {
index.insert_obstacles(entity, vec![Obstacle { entity, kind: ObstacleKind::Stump, capsule: stump.capsule.placed(place) }]);
}
for (entity, log, place) in &logs {
index.insert_obstacles(entity, vec![Obstacle { entity, kind: ObstacleKind::Log, capsule: log.capsule.placed(place) }]);
}
let lying_wood = |entity: Entity, shape: &TreeShape, cut_y: f32, place: &GlobalTransform| -> Vec<Obstacle> {
shape
.capsules()
.filter_map(|capsule| capsule.above(cut_y))
.map(|capsule| Obstacle { entity, kind: ObstacleKind::Lying, capsule: capsule.placed(place) })
.collect()
};
for (entity, fell, shape, place) in &felled {
let wood = if fell.at_rest { lying_wood(entity, shape, fell.cut_y(), place) } else { Vec::new() };
index.insert_obstacles(entity, wood);
}
for (entity, wood, shape, place) in &lying {
index.insert_obstacles(entity, lying_wood(entity, shape, wood.cut_y, place));
}
}
pub(crate) fn build(app: &mut bevy::app::App) {
app.init_resource::<FloraIndex>();
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::math::Dir3;
fn tree(entity: Entity, x: f32, z: f32) -> IndexedTree {
IndexedTree { entity, base: Vec3::new(x, 0.0, z), axis: Vec3::Y, height: 6.0, base_radius: 0.5, top_radius: 0.25, undergrowth: false }
}
#[test]
fn a_ray_meets_the_trunk_on_its_bark_and_misses_above_and_beside_it() {
let trunk = tree(World::new().spawn_empty().id(), 0.0, 0.0);
let hit = trunk.cast(Ray3d::new(Vec3::new(-10.0, 2.0, 0.0), Dir3::X), 50.0).expect("a hit");
let radius = 0.5 - 0.25 * 2.0 / 6.0;
assert!((hit.point.x + radius).abs() < 1e-4, "hit at {}", hit.point);
assert!((hit.distance - (10.0 - radius)).abs() < 1e-4);
assert!(hit.normal.x < -0.99 && hit.normal.y > 0.0, "normal {}", hit.normal);
let inside = trunk.cast(Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::Z), 5.0).expect("from inside");
assert!(inside.point.z > 0.0);
for (origin, dir, max) in [
(Vec3::new(-10.0, 6.5, 0.0), Dir3::X, 50.0),
(Vec3::new(-10.0, 2.0, 0.6), Dir3::X, 50.0),
(Vec3::new(-10.0, 2.0, 0.0), Dir3::X, 9.0),
(Vec3::new(-10.0, 2.0, 0.0), Dir3::NEG_X, 50.0),
] {
assert_eq!(trunk.cast(Ray3d::new(origin, dir), max), None, "{origin} {dir:?}");
}
}
#[test]
fn the_grid_finds_trees_near_a_point_and_along_a_ray_across_cells() {
let mut world = World::new();
let ids: Vec<Entity> = (0..11).map(|_| world.spawn_empty().id()).collect();
let mut index = FloraIndex::default();
for (i, &id) in ids[..10].iter().enumerate() {
index.insert(tree(id, -20.0 + 5.0 * i as f32, 3.0));
}
index.insert(tree(ids[10], 8.0, 8.0));
assert_eq!(index.len(), 11);
let mut near: Vec<Entity> = index.within(Vec2::new(0.0, 3.0), 5.6).map(|t| t.entity).collect();
near.sort();
let mut want = vec![ids[3], ids[4], ids[5]];
want.sort();
assert_eq!(near, want, "the trees within 5.6 m of (0, 3)");
assert_eq!(index.within(Vec2::new(8.0, 8.0), 1.0).count(), 1, "a tree in four cells is met once");
assert_eq!(index.nearest(Vec2::new(1.0, 4.0), 10.0).map(|t| t.entity), Some(ids[4]));
assert!(index.nearest(Vec2::new(100.0, 100.0), 10.0).is_none());
let hit = index.cast_ray(Ray3d::new(Vec3::new(40.0, 1.0, 3.0), Dir3::NEG_X), 100.0).expect("a hit");
assert_eq!(hit.tree, ids[9]);
let from = Vec3::new(-6.0, 1.0, -6.0);
let toward = Dir3::new(Vec3::new(8.0, 1.0, 8.0) - from).unwrap();
assert_eq!(index.cast_ray(Ray3d::new(from, toward), 50.0).map(|h| h.tree), Some(ids[10]));
index.remove(ids[10]);
assert!(index.within(Vec2::new(8.0, 8.0), 1.0).next().is_none());
assert!(index.cells.values().all(|list| !list.contains(&ids[10])));
}
#[test]
fn the_index_follows_trees_as_they_are_planted_grown_moved_felled_and_despawned() {
use crate::{FellTree, LodSettings, TreePreset, build_pool};
use bevy::asset::{AssetApp, AssetPlugin};
use bevy::mesh::Mesh;
use bevy::pbr::StandardMaterial;
use bevy::time::TimeUpdateStrategy;
use bevy::transform::components::Transform;
let mut app = bevy::app::App::new();
app.add_plugins((bevy::MinimalPlugins, AssetPlugin::default(), bevy::transform::TransformPlugin));
app.init_asset::<Mesh>();
app.init_asset::<StandardMaterial>();
app.add_plugins(crate::FloraPlugin);
app.insert_resource(TimeUpdateStrategy::ManualDuration(std::time::Duration::from_secs_f32(1.0 / 30.0)));
let oak = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), Transform::from_xyz(10.0, 0.0, 0.0))).id();
let birch_params = TreePreset::Birch.params();
let birch = app
.world_mut()
.spawn((ProceduralTree(birch_params.clone()), TreeAge(2.0), Transform::from_xyz(-10.0, 0.0, 0.0)))
.id();
let pool = build_pool(&TreePreset::Pine.params(), 2, f32::INFINITY, &LodSettings::default(), &mut app.world_mut().resource_mut::<Assets<Mesh>>());
let pool = app.world_mut().resource_mut::<Assets<TreePool>>().add(pool);
let pine = app.world_mut().spawn((PooledTree { pool, variant: 1 }, Transform::from_xyz(0.0, 0.0, 20.0))).id();
app.update();
app.update();
let index = app.world().resource::<FloraIndex>();
assert_eq!(index.len(), 3);
assert_eq!(index.nearest(Vec2::new(9.0, 0.0), 5.0).map(|t| t.entity), Some(oak));
assert_eq!(index.nearest(Vec2::new(0.5, 20.0), 2.0).map(|t| t.entity), Some(pine));
let young = *index.get(birch).expect("the birch is indexed");
assert!(young.height > 0.0 && young.height < birch_params.trunk.height, "a young trunk {} m tall", young.height);
let hit = index.cast_ray(Ray3d::new(Vec3::new(0.0, 1.0, 0.0), bevy::math::Dir3::X), 50.0).expect("the oak is hit");
let oak_tree = *index.get(oak).unwrap();
assert_eq!(hit.tree, oak);
assert!((hit.point.x - (10.0 - oak_tree.radius_at(1.0))).abs() < 0.01 && hit.normal.x < -0.9, "{hit:?}");
app.world_mut().get_mut::<TreeAge>(birch).unwrap().0 = 6.0;
app.update();
let grown = *app.world().resource::<FloraIndex>().get(birch).unwrap();
assert!(grown.height > young.height && grown.base_radius > young.base_radius, "{young:?} -> {grown:?}");
app.world_mut().get_mut::<Transform>(oak).unwrap().translation = Vec3::new(30.0, 0.0, 0.0);
app.update();
let index = app.world().resource::<FloraIndex>();
assert!(index.nearest(Vec2::new(9.0, 0.0), 5.0).is_none());
assert_eq!(index.nearest(Vec2::new(29.0, 0.0), 5.0).map(|t| t.entity), Some(oak));
app.world_mut().entity_mut(pine).insert(FellTree { speed: 4.0, ..Default::default() });
for _ in 0..300 {
app.update();
}
assert!(app.world().get::<Felled>(pine).is_some(), "the pine was not felled");
assert!(app.world().resource::<FloraIndex>().get(pine).is_none(), "a felled tree is still indexed");
app.world_mut().despawn(oak);
app.update();
let index = app.world().resource::<FloraIndex>();
assert!(index.get(oak).is_none());
assert_eq!(index.len(), 1);
}
#[test]
fn a_query_answers_in_an_order_set_by_where_the_trees_stand() {
let spots: Vec<(f32, f32)> = (0..12).map(|i| ((i % 4) as f32 * 3.0 - 4.0, (i / 4) as f32 * 3.0 - 2.0)).collect();
let ask = |reverse: bool| -> (Vec<(f32, f32)>, (f32, f32)) {
let mut world = World::new();
let mut index = FloraIndex::default();
let mut placed = spots.clone();
if reverse {
placed.reverse();
for _ in 0..7 {
world.spawn_empty();
}
}
for (x, z) in placed {
index.insert(tree(world.spawn_empty().id(), x, z));
}
let near = index.within(Vec2::ZERO, 20.0).map(|t| (t.base.x, t.base.z)).collect();
let tie = index.nearest(Vec2::new(-2.5, -2.0), 5.0).map(|t| (t.base.x, t.base.z)).unwrap();
(near, tie)
};
let (forward, backward) = (ask(false), ask(true));
assert_eq!(forward.0.len(), 12);
assert_eq!(forward, backward);
}
#[test]
fn undergrowth_is_told_apart_from_trees() {
use crate::{LodSettings, TreePreset, build_pool};
use bevy::asset::{AssetApp, AssetPlugin, Assets};
use bevy::mesh::Mesh;
use bevy::pbr::StandardMaterial;
use bevy::transform::components::Transform;
let mut app = bevy::app::App::new();
app.add_plugins((bevy::MinimalPlugins, AssetPlugin::default(), bevy::transform::TransformPlugin));
app.init_asset::<Mesh>();
app.init_asset::<StandardMaterial>();
app.add_plugins(crate::FloraPlugin);
let oak = app.world_mut().spawn((ProceduralTree(TreePreset::Oak.params()), Transform::default())).id();
let fern = app.world_mut().spawn((ProceduralTree(TreePreset::Fern.params()), Transform::from_xyz(3.0, 0.0, 0.0))).id();
let pool = {
let built = app.world_mut().resource_scope(|_, mut meshes: bevy::ecs::world::Mut<Assets<Mesh>>| {
build_pool(&TreePreset::Juniper.params(), 1, f32::INFINITY, &LodSettings::default(), &mut meshes)
});
app.world_mut().resource_mut::<Assets<TreePool>>().add(built)
};
let juniper = app.world_mut().spawn((PooledTree { pool, variant: 0 }, Transform::from_xyz(6.0, 0.0, 0.0))).id();
app.update();
app.update();
let index = app.world().resource::<FloraIndex>();
assert!(!index.get(oak).unwrap().undergrowth);
assert!(index.get(fern).unwrap().undergrowth && index.get(juniper).unwrap().undergrowth);
let trees: Vec<Entity> = index.within(Vec2::ZERO, 10.0).filter(|t| !t.undergrowth).map(|t| t.entity).collect();
assert_eq!(trees, vec![oak]);
}
}