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))]
}
}
}