//! Material asset loading and inspector/browser integration. use bevy::prelude::*; use bevy_egui::egui; use shared::{ ComponentInstanceId, LevelObject, MaterialAsset, MaterialDesc, MaterialRef, RendererMaterialSlot, }; use crate::assets::{EditorAsset, EditorAssetKind, EditorAssets}; use crate::history::{reflected_component_transaction, set_material_group_with_history}; use crate::ui::inspector::property_row; pub fn load_material_asset(catalog_path: &str) -> Result { MaterialAsset::load_from_path(catalog_path) } pub fn material_desc_from_asset(catalog_path: &str) -> Result { let asset = load_material_asset(catalog_path)?; let mut desc = asset.material; desc.material_asset_path = Some(catalog_path.to_string()); Ok(desc) } pub fn apply_material_asset_to_selection( world: &mut World, asset: &EditorAsset, selected: &bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities, ) -> Result<(), String> { let path = asset .path .clone() .ok_or_else(|| format!("Material has no source path: {}", asset.label))?; let record = world .get_resource::() .and_then(|registry| crate::asset_db::find_asset_by_path(registry, &path)) .ok_or_else(|| format!("Material is missing from the asset registry: {path}"))?; let sub_asset_id = if shared::MaterialInstanceAsset::load_from_path(&path).is_ok() { "material:instance" } else { "material:source" }; let material_ref = MaterialRef::new( shared::EditorAssetRef::new(record.id.as_string(), sub_asset_id, asset.label.clone()) .with_source_path(path.clone()), ); let desc = material_desc_from_asset(&path).ok(); let entities = crate::ui::helpers::selected_level_entities(world, selected); let mut renderer_changes = 0usize; let mut legacy_changes = Vec::new(); for entity in entities { if world.get::(entity).is_none() { continue; } if let Some(mut renderer) = world.get::(entity).cloned() { ensure_static_material_slots(&mut renderer); for slot in &mut renderer.materials.slots { slot.material = Some(material_ref.clone()); } reflected_component_transaction( world, entity, "Assign Material Slots", shared::AUTHORING_COMPONENT_STATIC_MESH_RENDERER, shared::COMPONENT_STATIC_MESH_RENDERER, move |world, entity| { world.entity_mut(entity).insert(renderer); Ok(()) }, )?; renderer_changes += 1; continue; } if let Some(mut renderer) = world.get::(entity).cloned() { if renderer.materials.slots.is_empty() { return Err(format!( "Skinned renderer `{}` has no imported material slots; reimport its model", renderer.path )); } for slot in &mut renderer.materials.slots { slot.material = Some(material_ref.clone()); } reflected_component_transaction( world, entity, "Assign Material Slots", shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER, shared::COMPONENT_SKINNED_MESH_RENDERER, move |world, entity| { world.entity_mut(entity).insert(renderer); Ok(()) }, )?; renderer_changes += 1; continue; } if let Some(desc) = desc.as_ref() { legacy_changes.push((entity, desc.clone())); } } if !legacy_changes.is_empty() { set_material_group_with_history(world, legacy_changes); } if renderer_changes == 0 && desc.is_none() { return Err("No selected renderable actor accepts this material".into()); } world.resource_mut::().status = format!("Applied shared material {}", asset.label); Ok(()) } fn ensure_static_material_slots(renderer: &mut shared::StaticMeshRenderer) { for part in &mut renderer.slots { if part.material_slot_id.0.trim().is_empty() { part.material_slot_id = if part.id.0.trim().is_empty() { ComponentInstanceId::new("slot:legacy") } else { ComponentInstanceId::new(format!("slot:{}", part.id.0)) }; } if renderer.materials.slot(&part.material_slot_id).is_none() { renderer.materials.slots.push(RendererMaterialSlot { id: part.material_slot_id.clone(), name: part.name.clone(), source_material: part.material.clone().map(MaterialRef::new), material: None, }); } } } pub fn material_asset_picker_ui( world: &mut World, ui: &mut egui::Ui, _entity: Entity, material: &mut MaterialDesc, _original: &MaterialDesc, ) -> bool { let assets: Vec<(String, String)> = world .resource::() .assets .iter() .filter(|a| matches!(a.kind, EditorAssetKind::Material)) .filter_map(|a| Some((a.label.clone(), a.path.clone()?))) .collect(); let current = material.material_asset_path.clone().unwrap_or_default(); let mut selected_label = assets .iter() .find(|(_, path)| path == ¤t) .map(|(label, _)| label.clone()) .unwrap_or_else(|| "(none)".to_string()); let mut changed = false; property_row(ui, "Material asset", |ui| { egui::ComboBox::from_id_salt("material_asset_picker") .selected_text(&selected_label) .show_ui(ui, |ui| { if ui.selectable_label(current.is_empty(), "(none)").clicked() { material.material_asset_path = None; selected_label = "(none)".into(); changed = true; } for (label, path) in &assets { if ui .selectable_label(&selected_label == label, label) .clicked() { if let Ok(desc) = material_desc_from_asset(path) { *material = desc; selected_label = label.clone(); changed = true; } else { world.resource_mut::().status = format!("Could not load material {path}"); } } } }); }); changed }