use crate::gltf_fetch::{self, PendingGltfMulti};
use nightshade::prelude::{ehttp, serde_json};
use serde::Deserialize;
use std::sync::{Arc, Mutex};
const ASSETS_URL: &str = "https://api.polyhaven.com/assets";
const FILES_URL_PREFIX: &str = "https://api.polyhaven.com/files/";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
Hdris,
Models,
}
impl Category {
fn query_param(self) -> &'static str {
match self {
Self::Hdris => "hdris",
Self::Models => "models",
}
}
}
#[derive(Debug, Clone, Deserialize)]
struct RawAsset {
name: String,
}
#[derive(Debug, Clone)]
pub struct AssetEntry {
pub slug: String,
pub name: 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 PendingHdr {
pub display_name: String,
pub bytes: Vec<u8>,
}
#[derive(Deserialize, Debug)]
struct FileLink {
url: String,
}
#[derive(Deserialize, Debug)]
struct HdriResolutionFiles {
#[serde(default)]
hdr: Option<FileLink>,
}
#[derive(Deserialize, Debug)]
struct HdriFiles {
hdri: std::collections::BTreeMap<String, HdriResolutionFiles>,
}
#[derive(Deserialize, Debug)]
struct IncludeFile {
url: String,
}
#[derive(Deserialize, Debug)]
struct GltfFile {
url: String,
#[serde(default)]
include: std::collections::BTreeMap<String, IncludeFile>,
}
#[derive(Deserialize, Debug)]
struct GltfResolution {
gltf: GltfFile,
}
#[derive(Deserialize, Debug)]
struct ModelFiles {
gltf: std::collections::BTreeMap<String, GltfResolution>,
}
struct CategoryState {
index: Arc<Mutex<IndexState>>,
}
impl CategoryState {
fn new() -> Self {
Self {
index: Arc::new(Mutex::new(IndexState::Idle)),
}
}
}
pub struct PolyhavenBrowser {
hdris: CategoryState,
models: CategoryState,
preferred_resolution: u32,
asset_loading: Arc<Mutex<Option<String>>>,
pending_hdr: Arc<Mutex<Option<PendingHdr>>>,
pending_gltf: Arc<Mutex<Option<PendingGltfMulti>>>,
}
impl Default for PolyhavenBrowser {
fn default() -> Self {
Self {
hdris: CategoryState::new(),
models: CategoryState::new(),
preferred_resolution: 1,
asset_loading: Arc::new(Mutex::new(None)),
pending_hdr: Arc::new(Mutex::new(None)),
pending_gltf: Arc::new(Mutex::new(None)),
}
}
}
impl PolyhavenBrowser {
pub fn take_pending_hdr(&mut self) -> Option<PendingHdr> {
self.pending_hdr.lock().ok().and_then(|mut p| p.take())
}
pub fn take_pending_gltf(&mut self) -> Option<PendingGltfMulti> {
self.pending_gltf.lock().ok().and_then(|mut p| p.take())
}
pub fn ensure_loaded(&self, category: Category) {
let state = self.category_state(category);
if matches!(&*state.index.lock().unwrap(), IndexState::Idle) {
self.start_index_fetch(category);
}
}
pub fn entries(&self, category: Category) -> Option<Arc<Vec<AssetEntry>>> {
let state = self.category_state(category);
let guard = state.index.lock().ok()?;
if let IndexState::Loaded(entries) = &*guard {
Some(Arc::clone(entries))
} else {
None
}
}
pub fn index_error(&self, category: Category) -> Option<String> {
let state = self.category_state(category);
let guard = state.index.lock().ok()?;
guard.error_message().map(str::to_string)
}
pub fn loading_status(&self) -> Option<String> {
self.asset_loading
.lock()
.ok()
.and_then(|guard| guard.clone())
}
pub fn fetch_asset(&self, category: Category, slug: &str, display_name: &str) {
if self.asset_loading.lock().unwrap().is_some() {
return;
}
*self.asset_loading.lock().unwrap() = Some(display_name.to_string());
let url = format!("{}{}", FILES_URL_PREFIX, slug);
let preferred = self.preferred_resolution;
match category {
Category::Hdris => self.fetch_hdri(url, display_name.to_string(), preferred),
Category::Models => self.fetch_model(url, display_name.to_string(), preferred),
}
}
pub fn set_preferred_resolution(&mut self, resolution: u32) {
self.preferred_resolution = resolution;
}
fn category_state(&self, category: Category) -> &CategoryState {
match category {
Category::Hdris => &self.hdris,
Category::Models => &self.models,
}
}
fn start_index_fetch(&self, category: Category) {
let state = self.category_state(category);
*state.index.lock().unwrap() = IndexState::Loading;
let index = Arc::clone(&state.index);
let url = format!("{}?type={}", ASSETS_URL, category.query_param());
ehttp::fetch(ehttp::Request::get(&url), move |result| {
let next = match result {
Ok(resp) if resp.ok => {
match serde_json::from_slice::<std::collections::BTreeMap<String, RawAsset>>(
&resp.bytes,
) {
Ok(raw) => IndexState::Loaded(Arc::new(into_entries(raw))),
Err(error) => IndexState::Failed(error.to_string()),
}
}
Ok(resp) => IndexState::Failed(format!("HTTP {}", resp.status)),
Err(error) => IndexState::Failed(error),
};
if let Ok(mut state) = index.lock() {
*state = next;
}
});
}
fn fetch_hdri(&self, files_url: String, display_name: String, preferred: u32) {
let pending = Arc::clone(&self.pending_hdr);
let loading = Arc::clone(&self.asset_loading);
ehttp::fetch(
ehttp::Request::get(&files_url),
move |result: ehttp::Result<ehttp::Response>| {
let parsed = result
.ok()
.filter(|r| r.ok)
.and_then(|r| serde_json::from_slice::<HdriFiles>(&r.bytes).ok());
let url_opt = parsed.and_then(|files| {
let entries: Vec<(u32, String)> = files
.hdri
.into_iter()
.filter_map(|(key, res)| {
res.hdr.map(|link| (resolution_value(&key), link.url))
})
.collect();
pick_resolution(entries, preferred)
});
match url_opt {
Some(url) => download_hdr(url, display_name, pending, loading),
None => {
if let Ok(mut state) = loading.lock() {
*state = None;
}
}
}
},
);
}
fn fetch_model(&self, files_url: String, display_name: String, preferred: u32) {
let pending = Arc::clone(&self.pending_gltf);
let loading = Arc::clone(&self.asset_loading);
ehttp::fetch(
ehttp::Request::get(&files_url),
move |result: ehttp::Result<ehttp::Response>| {
let parsed = result
.ok()
.filter(|r| r.ok)
.and_then(|r| serde_json::from_slice::<ModelFiles>(&r.bytes).ok());
let pick = parsed.and_then(|files| {
let entries: Vec<(u32, GltfFile)> = files
.gltf
.into_iter()
.map(|(key, res)| (resolution_value(&key), res.gltf))
.collect();
pick_resolution(entries, preferred)
});
match pick {
Some(gltf) => {
let resources: Vec<(String, String)> = gltf
.include
.into_iter()
.map(|(key, file)| (key, file.url))
.collect();
gltf_fetch::start_multi_fetch(
gltf.url,
resources,
display_name,
pending,
loading,
);
}
None => {
if let Ok(mut state) = loading.lock() {
*state = None;
}
}
}
},
);
}
}
fn pick_resolution<T>(mut entries: Vec<(u32, T)>, preferred: u32) -> Option<T> {
entries.sort_by_key(|(value, _)| *value);
if let Some(index) = entries.iter().position(|(value, _)| *value == preferred) {
return Some(entries.swap_remove(index).1);
}
let upper_bound_index = entries.iter().rposition(|(value, _)| *value <= preferred);
match upper_bound_index {
Some(index) => Some(entries.swap_remove(index).1),
None => entries.into_iter().next().map(|(_, value)| value),
}
}
fn into_entries(raw: std::collections::BTreeMap<String, RawAsset>) -> Vec<AssetEntry> {
let mut entries: Vec<AssetEntry> = raw
.into_iter()
.map(|(slug, asset)| AssetEntry {
slug,
name: asset.name,
})
.collect();
entries.sort_by_key(|entry| entry.name.to_lowercase());
entries
}
fn resolution_value(key: &str) -> u32 {
let trimmed = key.trim_end_matches('k').trim_end_matches('K');
trimmed.parse::<u32>().unwrap_or(u32::MAX)
}
fn download_hdr(
url: String,
display_name: String,
pending: Arc<Mutex<Option<PendingHdr>>>,
loading: Arc<Mutex<Option<String>>>,
) {
ehttp::fetch(
ehttp::Request::get(&url),
move |result: ehttp::Result<ehttp::Response>| {
if let Ok(resp) = result
&& resp.ok
&& let Ok(mut slot) = pending.lock()
{
*slot = Some(PendingHdr {
display_name,
bytes: resp.bytes,
});
}
if let Ok(mut state) = loading.lock() {
*state = None;
}
},
);
}