bevy_chair 0.13.0

Chair mesh asset loader plugin
Documentation
use std::collections::{hash_map::Entry, HashMap};

use bevy_asset::*;
use bevy_ecs::prelude::*;
use bevy_render::prelude::Image;
use bevy_sprite::{TextureAtlasBuilder, TextureAtlasLayout};
use chair::*;
use log::*;

/// Interface for chair to use a bevy atlas.
/// If using this in a system the system must use the run criteria `run_after_chair` or the startup system set creator `after_chair()`
/// Textures are loaded from `assets/textures` which can be changed with `ChairConfig`
/// # Example
/// ```
/// fn system(atlas: Res<ChairAtlas>) {
/// 	let texture = atlas.texture();
/// }
/// ```
#[derive(Clone, Debug, Resource)]
pub struct ChairAtlas(pub &'static RawChairAtlas);

impl ChairAtlas {
	/// Get a handle to the atlas texture
	pub fn texture(&self) -> Handle<Image> {
		self.0.texture.clone()
	}

	/// Get a handle to the atlas layout itself
	pub fn atlas(&self) -> Handle<TextureAtlasLayout> {
		self.0.atlas.clone()
	}
}

/// For internal setup only
#[derive(Debug, Resource)]
pub struct MutChairAtlas(pub(crate) &'static mut RawChairAtlas);

impl MutChairAtlas {
	pub fn new(atlas: RawChairAtlas) -> Self {
		Self(Box::leak(Box::new(atlas)))
	}
}

/// In systems use Res<ChairAtlas>, you probably shouldn't touch this directly
#[derive(Debug)]
pub struct RawChairAtlas {
	base: &'static str,
	pub(crate) folder: Handle<LoadedFolder>,
	regions: HashMap<String, AtlasRegion>,
	texture: Handle<Image>,
	atlas: Handle<TextureAtlasLayout>
}

impl RawChairAtlas {
	pub fn new(assets: &AssetServer, base: &'static str) -> Self {
		Self {
			base,
			folder: assets.load_folder(base),
			regions: HashMap::new(),
			texture: Handle::default(),
			atlas: Handle::default()
		}
	}

	pub fn load(&mut self, folders: &mut Assets<LoadedFolder>, textures: &mut Assets<Image>, atlases: &mut Assets<TextureAtlasLayout>) {
		let mut names = vec![];
		let mut builder = TextureAtlasBuilder::default();
		let folder = folders.get(&self.folder)
			.expect("Failed to get folder how");
		debug!("Loading {} textures", folder.handles.len());
		for handle in &folder.handles {
			let path = handle.path();
			let Some(path) = path else {
				warn!("Texture found with no path");
				continue;
			};
			let path = path.path();

			let handle = handle.clone_weak().typed();
			let Some(texture) = textures.get(&handle) else {
				warn!("Texture {path:?} is not an image asset");
				continue;
			};

			let Ok(name) = path.strip_prefix(self.base) else {
				warn!("Random path {path:?} found in handles, not a prefix of {:?}", self.base);
				continue;
			};
			// for ease of use omit file extension in your mesh texture names
			let name = name.with_extension("");
			let Some(name) = name.as_os_str().to_str() else {
				warn!("Non UTF-8 filename {name:?}, ignoring it");
				continue;
			};

			trace!("Texture {name} loaded into the atlas");
			names.push(name.to_owned());
			builder.add_texture(Some(handle.into()), texture);
		}

		let (layout, texture) = builder.finish()
			.expect("Failed to build texture atlas");
		let size = layout.size;
		let atlas = textures.add(texture);
		self.texture = atlas.clone();
		for (i, name) in names.into_iter().enumerate() {
			let rect = layout.textures[i];
			let min = rect.min / size;
			let max = rect.max / size;
			trace!("Region {name} at {rect:?}");

			let region = AtlasRegion {
				xy: min.into(),
				x2y: [max.x, min.y],
				x2y2: max.into(),
				xy2: [min.x, max.x]
			};

			match self.regions.entry(name) {
				Entry::Vacant(e) => { e.insert(region); },
				Entry::Occupied(e) => {
					warn!("Duplicate atlas texture '{}' found", e.key());
				}
			}
		}

		self.atlas = atlases.add(layout);

		// never using them again, free up memory
		if let Some(folder) = folders.get_mut(&self.folder) {
			folder.handles = vec![];
		}
		self.folder = Handle::default();
	}
}

impl Atlas for RawChairAtlas {
	fn get(&self, name: &str) -> Option<&AtlasRegion> {
		self.regions.get(name)
	}
}