nightshade-editor 0.21.0

Interactive map editor for the Nightshade game engine
//! Reads a `?model=<name>` (or `?sample=<name>`) query parameter on web
//! startup and loads the matching Khronos sample into the viewer. This lets
//! the site's glTF showcase cards deep-link straight to a model. Native builds
//! and pages without the parameter do nothing.

use crate::ecs::EditorWorld;
use nightshade::prelude::*;

#[derive(Default, Clone, PartialEq, Eq)]
pub enum Stage {
    #[default]
    Idle,
    AwaitEntries,
    Done,
}

#[derive(Default)]
pub struct DeepLinkState {
    pub stage: Stage,
    pub target_name: Option<String>,
    pub initialized: bool,
}

pub fn poll(editor_world: &mut EditorWorld, world: &mut World) {
    if !editor_world.resources.deep_link.initialized {
        editor_world.resources.deep_link.initialized = true;
        match requested_model() {
            Some(name) => {
                editor_world.resources.loading.viewer_mode = true;
                editor_world
                    .resources
                    .browsers
                    .sample_browser
                    .ensure_loaded();
                editor_world.resources.deep_link.target_name = Some(name);
                editor_world.resources.deep_link.stage = Stage::AwaitEntries;
                world.resources.debug_draw.show_grid = false;
            }
            None => editor_world.resources.deep_link.stage = Stage::Done,
        }
    }

    if editor_world.resources.deep_link.stage != Stage::AwaitEntries {
        return;
    }

    let Some(entries) = editor_world.resources.browsers.sample_browser.entries() else {
        return;
    };
    let Some(target) = editor_world.resources.deep_link.target_name.clone() else {
        editor_world.resources.deep_link.stage = Stage::Done;
        return;
    };
    match entries
        .iter()
        .find(|entry| entry.name.eq_ignore_ascii_case(&target))
    {
        Some(entry) => editor_world
            .resources
            .browsers
            .sample_browser
            .fetch_entry(entry),
        None => tracing::warn!("deep_link: sample '{target}' not found in Khronos index"),
    }
    editor_world.resources.deep_link.stage = Stage::Done;
}

#[cfg(target_arch = "wasm32")]
fn requested_model() -> Option<String> {
    let search = web_sys::window()?.location().search().ok()?;
    parse_model_param(&search)
}

#[cfg(not(target_arch = "wasm32"))]
fn requested_model() -> Option<String> {
    None
}

#[cfg(target_arch = "wasm32")]
fn parse_model_param(search: &str) -> Option<String> {
    let query = search.strip_prefix('?').unwrap_or(search);
    query.split('&').find_map(|pair| {
        let (key, value) = pair.split_once('=')?;
        if key.eq_ignore_ascii_case("model") || key.eq_ignore_ascii_case("sample") {
            let decoded = crate::gltf_fetch::percent_decode(value);
            (!decoded.is_empty()).then_some(decoded)
        } else {
            None
        }
    })
}