nightshade 0.8.2

A cross-platform data-oriented game engine.
Documentation
use super::context::EditorContext;
#[cfg(all(feature = "fbx", not(target_arch = "wasm32")))]
use super::selection::EntitySelection;
use crate::ecs::prefab::resources::mesh_cache_insert;
#[cfg(all(feature = "fbx", not(target_arch = "wasm32")))]
use crate::mosaic::ToastKind;
use crate::prelude::*;

pub struct GltfImportResult {
    pub tree_dirty: bool,
    pub prefabs: Vec<crate::ecs::prefab::Prefab>,
    pub animations: Vec<crate::ecs::animation::components::AnimationClip>,
    pub source_path: String,
}

pub fn load_gltf_resources(
    world: &mut World,
    textures: std::collections::HashMap<String, (Vec<u8>, u32, u32)>,
    meshes: std::collections::HashMap<String, crate::ecs::mesh::Mesh>,
) {
    for (name, (rgba_data, width, height)) in textures {
        world.queue_command(WorldCommand::LoadTexture {
            name,
            rgba_data,
            width,
            height,
        });
    }

    for (name, mesh) in meshes {
        mesh_cache_insert(&mut world.resources.mesh_cache, name, mesh);
    }
}

fn get_viewport_spawn_position(world: &World) -> Vec3 {
    if let Some(camera_entity) = world.resources.active_camera
        && let Some(transform) = world.get_global_transform(camera_entity)
    {
        let forward = transform.forward_vector();
        return transform.translation() + forward * 5.0;
    }
    Vec3::zeros()
}

pub fn spawn_asset_at_viewport(
    context: &mut EditorContext,
    world: &mut World,
    path: &std::path::Path,
) -> bool {
    let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
    let lower_ext = extension.to_lowercase();

    if !matches!(lower_ext.as_str(), "gltf" | "glb" | "fbx" | "obj") {
        return false;
    }

    let position = get_viewport_spawn_position(world);

    if (lower_ext == "gltf" || lower_ext == "glb")
        && let Ok(result) = crate::ecs::prefab::import_gltf_from_path(path)
    {
        load_gltf_resources(world, result.textures, result.meshes);

        let source_path = path
            .canonicalize()
            .ok()
            .and_then(|p| p.to_str().map(|s| s.to_string()));

        if let Some(prefab) = result.prefabs.first() {
            let entity = crate::ecs::prefab::spawn_prefab_with_source(
                world,
                prefab,
                &result.animations,
                &result.skins,
                position,
                source_path.as_deref(),
            );
            let hierarchy = Box::new(super::undo::capture_hierarchy(world, entity));
            context.undo_history.push(
                super::undo::UndoableOperation::EntityCreated {
                    hierarchy,
                    current_entity: entity,
                },
                "Spawn asset".to_string(),
            );
            context.selection.set_single(entity);
            world.resources.graphics.bounding_volume_selected_entity = Some(entity);
            return true;
        }
    }
    false
}

fn process_gltf_import(
    context: &mut EditorContext,
    world: &mut World,
    result: crate::ecs::prefab::GltfLoadResult,
    source_path: String,
) -> GltfImportResult {
    load_gltf_resources(world, result.textures, result.meshes);

    let mut spawned_entities = Vec::new();
    for prefab in &result.prefabs {
        let entity = crate::ecs::prefab::spawn_prefab_with_source(
            world,
            prefab,
            &result.animations,
            &result.skins,
            nalgebra_glm::vec3(0.0, 0.0, 0.0),
            Some(&source_path),
        );
        spawned_entities.push(entity);
    }

    if let Some(&last_entity) = spawned_entities.last() {
        context.selection.set_single(last_entity);
    }

    if spawned_entities.len() == 1 {
        let entity = spawned_entities[0];
        let hierarchy = Box::new(super::undo::capture_hierarchy(world, entity));
        context.undo_history.push(
            super::undo::UndoableOperation::EntityCreated {
                hierarchy,
                current_entity: entity,
            },
            "Import GLTF".to_string(),
        );
    } else if spawned_entities.len() > 1 {
        super::undo::push_bulk_entities_created(
            &mut context.undo_history,
            world,
            spawned_entities,
            "Import GLTF",
        );
    }

    GltfImportResult {
        tree_dirty: true,
        prefabs: result.prefabs,
        animations: result.animations,
        source_path,
    }
}

pub fn load_gltf(
    context: &mut EditorContext,
    world: &mut World,
    path: &std::path::Path,
) -> GltfImportResult {
    let source_path = path
        .canonicalize()
        .ok()
        .and_then(|absolute| absolute.to_str().map(|s| s.to_string()))
        .unwrap_or_else(|| path.display().to_string());

    match crate::ecs::prefab::import_gltf_from_path(path) {
        Ok(result) => process_gltf_import(context, world, result, source_path),
        Err(error) => {
            tracing::error!("Failed to load GLTF file: {}", error);
            GltfImportResult {
                tree_dirty: false,
                prefabs: Vec::new(),
                animations: Vec::new(),
                source_path,
            }
        }
    }
}

pub fn load_gltf_from_bytes(
    context: &mut EditorContext,
    world: &mut World,
    name: &str,
    data: &[u8],
) -> GltfImportResult {
    let source_path = name.to_string();

    match crate::ecs::prefab::import_gltf_from_bytes(data) {
        Ok(result) => process_gltf_import(context, world, result, source_path),
        Err(error) => {
            tracing::error!("Failed to load GLTF from bytes: {}", error);
            GltfImportResult {
                tree_dirty: false,
                prefabs: Vec::new(),
                animations: Vec::new(),
                source_path,
            }
        }
    }
}

#[cfg(all(feature = "fbx", not(target_arch = "wasm32")))]
pub fn import_fbx(world: &mut World) -> Vec<(ToastKind, String)> {
    let filters = [crate::filesystem::FileFilter {
        name: "FBX".to_string(),
        extensions: vec!["fbx".to_string(), "FBX".to_string()],
    }];
    let Some(fbx_path) = crate::filesystem::pick_file(&filters) else {
        return Vec::new();
    };

    load_fbx(world, &fbx_path)
}

#[cfg(all(feature = "fbx", not(target_arch = "wasm32")))]
pub fn load_fbx(world: &mut World, path: &std::path::Path) -> Vec<(ToastKind, String)> {
    let source_path = path
        .canonicalize()
        .ok()
        .and_then(|absolute| absolute.to_str().map(|s| s.to_string()))
        .unwrap_or_else(|| path.display().to_string());

    match crate::ecs::prefab::import_fbx_from_path(path) {
        Ok(result) => {
            let has_meshes = !result.meshes.is_empty();
            load_gltf_resources(world, result.textures, result.meshes);

            let fbx_namespace = path.file_stem().and_then(|s| s.to_str()).unwrap_or("fbx");

            for clip in &result.animations {
                let clip_name = format!("{}::{}", fbx_namespace, clip.name);
                crate::ecs::prefab::resources::animation_cache_insert(
                    &mut world.resources.animation_cache,
                    clip_name,
                    clip.clone(),
                );
            }

            if has_meshes {
                for prefab in result.prefabs {
                    let prefab_name = format!("{}::{}", fbx_namespace, prefab.name);
                    let cached = crate::ecs::prefab::resources::CachedPrefab {
                        prefab,
                        animations: result.animations.clone(),
                        skins: result.skins.clone(),
                        source_path: Some(source_path.clone()),
                    };
                    crate::ecs::prefab::resources::prefab_cache_insert(
                        &mut world.resources.prefab_cache,
                        prefab_name,
                        cached,
                    );
                }
            }

            vec![(
                ToastKind::Success,
                format!(
                    "Imported {} (View > Animations to instantiate)",
                    path.display()
                ),
            )]
        }
        Err(error) => {
            vec![(ToastKind::Error, format!("Failed to load FBX: {}", error))]
        }
    }
}

#[cfg(all(feature = "fbx", not(target_arch = "wasm32")))]
pub fn import_animation(
    selection: &EntitySelection,
    world: &mut World,
) -> Vec<(ToastKind, String)> {
    let Some(selected_entity) = selection.primary() else {
        return vec![(ToastKind::Error, "No entity selected".to_string())];
    };

    if !world.entity_has_components(selected_entity, crate::ecs::world::ANIMATION_PLAYER) {
        return vec![(
            ToastKind::Error,
            "Selected entity does not have an AnimationPlayer".to_string(),
        )];
    }

    let filters = [crate::filesystem::FileFilter {
        name: "FBX".to_string(),
        extensions: vec!["fbx".to_string(), "FBX".to_string()],
    }];
    let Some(fbx_path) = crate::filesystem::pick_file(&filters) else {
        return Vec::new();
    };

    match crate::ecs::prefab::import_fbx_from_path(&fbx_path) {
        Ok(load_result) => {
            if load_result.animations.is_empty() {
                return vec![(
                    ToastKind::Warning,
                    "No animations found in FBX file".to_string(),
                )];
            }

            if let Some(animation_player) = world.get_animation_player_mut(selected_entity) {
                let count = load_result.animations.len();
                animation_player.add_clips(load_result.animations);
                vec![(
                    ToastKind::Success,
                    format!("Added {} animation(s) from {}", count, fbx_path.display()),
                )]
            } else {
                Vec::new()
            }
        }
        Err(error) => {
            vec![(ToastKind::Error, format!("Failed to load FBX: {}", error))]
        }
    }
}