use crate::gltf_fetch::{self, PendingGltfMulti};
use nightshade::prelude::{ehttp, serde_json};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
const BASE_URL: &str =
"https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models";
const INDEX_URL: &str = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/model-index.json";
#[derive(Debug, Clone, Deserialize)]
struct RawEntry {
label: String,
name: String,
#[serde(default)]
variants: std::collections::BTreeMap<String, String>,
#[serde(default)]
screenshot: Option<String>,
}
#[derive(Debug, Clone)]
pub struct AssetEntry {
pub label: String,
pub name: String,
pub glb_url: Option<String>,
pub gltf_folder_filename: Option<String>,
pub screenshot_url: Option<String>,
}
enum IndexState {
Idle,
Loading,
Loaded(Arc<Vec<AssetEntry>>),
Failed(String),
}
impl IndexState {
fn error_message(&self) -> Option<&str> {
match self {
Self::Failed(message) => Some(message.as_str()),
_ => None,
}
}
}
pub struct PendingGlb {
pub display_name: String,
pub bytes: Vec<u8>,
}
pub struct SampleBrowser {
index: Arc<Mutex<IndexState>>,
pending_glb: Arc<Mutex<Option<PendingGlb>>>,
pending_gltf: Arc<Mutex<Option<PendingGltfMulti>>>,
glb_loading: Arc<Mutex<Option<String>>>,
}
impl Default for SampleBrowser {
fn default() -> Self {
Self {
index: Arc::new(Mutex::new(IndexState::Idle)),
pending_glb: Arc::new(Mutex::new(None)),
pending_gltf: Arc::new(Mutex::new(None)),
glb_loading: Arc::new(Mutex::new(None)),
}
}
}
impl SampleBrowser {
pub fn take_pending_gltf(&mut self) -> Option<PendingGltfMulti> {
self.pending_gltf.lock().ok().and_then(|mut p| p.take())
}
pub fn take_pending_glb(&mut self) -> Option<PendingGlb> {
self.pending_glb.lock().ok().and_then(|mut p| p.take())
}
pub fn ensure_loaded(&self) {
if matches!(&*self.index.lock().unwrap(), IndexState::Idle) {
self.start_index_fetch();
}
}
pub fn entries(&self) -> Option<Arc<Vec<AssetEntry>>> {
let guard = self.index.lock().ok()?;
if let IndexState::Loaded(entries) = &*guard {
Some(Arc::clone(entries))
} else {
None
}
}
pub fn index_error(&self) -> Option<String> {
let guard = self.index.lock().ok()?;
guard.error_message().map(str::to_string)
}
pub fn loading_status(&self) -> Option<String> {
self.glb_loading.lock().ok().and_then(|guard| guard.clone())
}
pub fn queue_thumbnails(&self, queue: &nightshade::prelude::SharedTextureQueue) {
let Some(entries) = self.entries() else {
return;
};
let Ok(mut q) = queue.lock() else {
return;
};
for entry in entries.iter() {
let Some(url) = entry.screenshot_url.clone() else {
continue;
};
let name = format!("khronos_thumb_{}", entry.name);
q.queue_texture(name, url);
}
}
pub fn fetch_entry(&self, entry: &AssetEntry) {
if let Some(url) = entry.glb_url.clone() {
self.start_glb_fetch(entry.label.clone(), url);
} else if let Some(filename) = entry.gltf_folder_filename.clone() {
self.start_gltf_folder_fetch(entry.name.clone(), entry.label.clone(), filename);
}
}
fn start_index_fetch(&self) {
*self.index.lock().unwrap() = IndexState::Loading;
let target = Arc::clone(&self.index);
ehttp::fetch(
ehttp::Request::get(INDEX_URL),
move |result: ehttp::Result<ehttp::Response>| {
let mut state = target.lock().unwrap();
*state = match result {
Ok(resp) if resp.ok => {
match serde_json::from_slice::<Vec<RawEntry>>(&resp.bytes) {
Ok(raw) => IndexState::Loaded(Arc::new(into_entries(raw))),
Err(error) => IndexState::Failed(format!("parse: {error}")),
}
}
Ok(resp) => {
IndexState::Failed(format!("HTTP {}: {}", resp.status, resp.status_text))
}
Err(error) => IndexState::Failed(error),
};
},
);
}
fn start_glb_fetch(&self, display_name: String, url: String) {
if self.glb_loading.lock().unwrap().is_some() {
return;
}
*self.glb_loading.lock().unwrap() = Some(display_name.clone());
let target = Arc::clone(&self.pending_glb);
let loading = Arc::clone(&self.glb_loading);
ehttp::fetch(
ehttp::Request::get(&url),
move |result: ehttp::Result<ehttp::Response>| {
if let Ok(resp) = result
&& resp.ok
&& let Ok(mut slot) = target.lock()
{
*slot = Some(PendingGlb {
display_name,
bytes: resp.bytes,
});
}
if let Ok(mut state) = loading.lock() {
*state = None;
}
},
);
}
fn start_gltf_folder_fetch(
&self,
asset_name: String,
display_name: String,
gltf_filename: String,
) {
if self.glb_loading.lock().unwrap().is_some() {
return;
}
*self.glb_loading.lock().unwrap() = Some(display_name.clone());
let folder_url = format!("{}/{}/glTF", BASE_URL, asset_name);
let gltf_url = format!("{}/{}", folder_url, gltf_filename);
let pending = Arc::clone(&self.pending_gltf);
let loading = Arc::clone(&self.glb_loading);
ehttp::fetch(
ehttp::Request::get(&gltf_url),
move |result: ehttp::Result<ehttp::Response>| {
let bytes = match result {
Ok(resp) if resp.ok => resp.bytes,
_ => {
if let Ok(mut state) = loading.lock() {
*state = None;
}
return;
}
};
let uris = gltf_fetch::external_uris_from_gltf(&bytes);
let resources: Vec<(String, String)> = uris
.into_iter()
.map(|uri| {
let key = gltf_fetch::percent_decode(&uri);
(key, format!("{}/{}", folder_url, uri))
})
.collect();
if let Ok(mut state) = loading.lock() {
*state = Some(display_name.clone());
}
let staged_pending = Arc::clone(&pending);
let staged_loading = Arc::clone(&loading);
let staged_pending_clone = Arc::clone(&staged_pending);
let staged_loading_clone = Arc::clone(&staged_loading);
let display_for_finalize = display_name.clone();
if resources.is_empty() {
if let Ok(mut slot) = staged_pending_clone.lock() {
*slot = Some(PendingGltfMulti {
display_name: display_for_finalize,
gltf_bytes: bytes,
resources: HashMap::new(),
});
}
if let Ok(mut state) = staged_loading_clone.lock() {
*state = None;
}
return;
}
let progress = Arc::new(Mutex::new((Some(bytes), HashMap::new(), false)));
let total = resources.len();
for (key, url) in resources {
let progress = Arc::clone(&progress);
let pending = Arc::clone(&staged_pending);
let loading = Arc::clone(&staged_loading);
let display = display_name.clone();
ehttp::fetch(
ehttp::Request::get(&url),
move |result: ehttp::Result<ehttp::Response>| {
let bytes = result.ok().filter(|r| r.ok).map(|r| r.bytes);
let mut prog = progress.lock().unwrap();
match bytes {
Some(b) => {
prog.1.insert(key.clone(), b);
}
None => prog.2 = true,
}
if prog.2 {
if let Ok(mut state) = loading.lock() {
*state = None;
}
return;
}
if prog.1.len() < total {
return;
}
let gltf_bytes = prog.0.take().unwrap();
let resources = std::mem::take(&mut prog.1);
if let Ok(mut slot) = pending.lock() {
*slot = Some(PendingGltfMulti {
display_name: display,
gltf_bytes,
resources,
});
}
if let Ok(mut state) = loading.lock() {
*state = None;
}
},
);
}
},
);
}
}
fn into_entries(raw: Vec<RawEntry>) -> Vec<AssetEntry> {
let mut entries: Vec<AssetEntry> = raw
.into_iter()
.map(|r| {
let glb_url = r
.variants
.get("glTF-Binary")
.map(|filename| format!("{}/{}/glTF-Binary/{}", BASE_URL, r.name, filename));
let gltf_folder_filename = r.variants.get("glTF").cloned();
let screenshot_url = r
.screenshot
.as_ref()
.map(|path| format!("{}/{}/{}", BASE_URL, r.name, path));
AssetEntry {
label: r.label,
name: r.name,
glb_url,
gltf_folder_filename,
screenshot_url,
}
})
.collect();
entries.retain(|entry| entry.glb_url.is_some() || entry.gltf_folder_filename.is_some());
entries.sort_by_key(|entry| entry.label.to_lowercase());
entries
}