use std::collections::HashMap;
use uuid::Uuid;
use super::material::{MaterialDef, TextureDef};
use super::mesh::RuntimeMesh;
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
#[derive(Eq, Hash, Copy)]
pub struct AssetId(pub Uuid);
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for AssetId {
fn schema_name() -> std::borrow::Cow<'static, str> {
"AssetId".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "string", "format": "uuid" })
}
}
impl AssetId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl Default for AssetId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for AssetId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum AssetSource {
Filename(String),
Url(String),
Material(MaterialDef),
Texture(TextureDef),
Mesh(RuntimeMesh),
}
impl AssetSource {
pub fn display_name(&self) -> Option<&str> {
match self {
Self::Filename(name) => Some(name.as_str()),
Self::Texture(crate::material::TextureDef::Raster { display_name }) => {
Some(display_name.as_str())
}
_ => None,
}
}
pub fn is_file_backed(&self) -> bool {
matches!(self, Self::Filename(_) | Self::Url(_))
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct AssetEntry {
pub source: AssetSource,
#[serde(default)]
pub gltf_material_asset_ids: Vec<AssetId>,
#[serde(default)]
pub gltf_image_asset_ids: Vec<AssetId>,
#[serde(default)]
pub content_hash: String,
}
impl AssetEntry {
pub fn new(source: AssetSource) -> Self {
Self {
source,
gltf_material_asset_ids: Vec::new(),
gltf_image_asset_ids: Vec::new(),
content_hash: String::new(),
}
}
pub fn new_with_hash(source: AssetSource, content_hash: String) -> Self {
Self {
source,
gltf_material_asset_ids: Vec::new(),
gltf_image_asset_ids: Vec::new(),
content_hash,
}
}
}
pub fn asset_filename(id: AssetId, entry: &AssetEntry) -> Option<String> {
use crate::material::{mesh_asset_filename, TextureDef};
if let AssetSource::Mesh(_) = &entry.source {
return Some(mesh_asset_filename(id));
}
if entry.content_hash.is_empty() {
return None;
}
let display = match &entry.source {
AssetSource::Filename(name) => name.as_str(),
AssetSource::Texture(TextureDef::Raster { display_name }) => display_name.as_str(),
_ => return None,
};
let ext = display.rsplit_once('.').map(|(_, e)| e).unwrap_or("");
Some(if ext.is_empty() {
entry.content_hash.clone()
} else {
format!("{}.{}", entry.content_hash, ext)
})
}
pub fn asset_disk_path(id: AssetId, entry: &AssetEntry) -> Option<String> {
asset_filename(id, entry).map(|leaf| format!("assets/{leaf}"))
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
#[serde(transparent)]
pub struct AssetTable {
pub entries: HashMap<AssetId, AssetEntry>,
}
impl AssetTable {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, id: AssetId) -> Option<&AssetEntry> {
self.entries.get(&id)
}
pub fn display_name(&self, id: AssetId) -> Option<&str> {
self.entries.get(&id).and_then(|e| e.source.display_name())
}
pub fn find_by_content_hash(&self, hash: &str) -> Option<AssetId> {
if hash.is_empty() {
return None;
}
self.entries
.iter()
.find_map(|(id, entry)| (entry.content_hash == hash).then_some(*id))
}
pub fn insert_file_with_hash(&mut self, display_name: String, content_hash: String) -> AssetId {
if let Some(id) = self.find_by_content_hash(&content_hash) {
return id;
}
let id = AssetId::new();
self.entries.insert(
id,
AssetEntry::new_with_hash(AssetSource::Filename(display_name), content_hash),
);
id
}
pub fn remove(&mut self, id: AssetId) {
self.entries.remove(&id);
}
}