nightshade-editor 0.14.2

Interactive map editor for the Nightshade game engine
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;
            }
        },
    );
}