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::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())
    }

    /// Queues a thumbnail texture fetch for every loaded asset entry. The
    /// texture name is `khronos_thumb_<asset_name>`. Callers look up the
    /// resulting texture index via the texture cache registry.
    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
}