use super::*; pub(super) fn skinned_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let mesh_candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Mesh); let material_candidates = brush_face_material_ref_candidates(world); let material_drop_candidate = world .get_resource::() .and_then(|assets| assets.dragging_selection().cloned()) .and_then(|selection| { asset_ref_candidate_from_selection(world, &selection, AssetRefCandidateKind::Material) }); let invalid_material_drop = material_drop_invalid_reason(world, material_drop_candidate.is_some()); let Some(mut renderer) = world.get::(entity).cloned() else { return; }; let original = renderer.clone(); let mut changed = false; let mut accepted_drop = false; let mut browse_mesh = false; let mut locate_mesh = false; let mut options = ComponentCardOptions::removable( COMPONENT_SKINNED_MESH_RENDERER, "Skinned Mesh Renderer", icons::PERSON_SIMPLE_RUN, ); options.active_toggle = false; options.removable = world.get::(entity).is_none(); options.resettable = false; options.copyable = false; options.summary = "Skeleton-bound renderer"; options.body_margin = 0; let context = component_card_context(world, entity, options); let response = component_card(ui, &context, |ui| { let renderer_open_id = ui.make_persistent_id(("skinned_renderer_open", entity)); let mut renderer_open = ui .ctx() .data_mut(|data| data.get_persisted::(renderer_open_id)) .unwrap_or(true); let source_candidate = mesh_candidates .iter() .find(|candidate| candidate.reference.asset_id == renderer.asset_id); let label = Path::new(&renderer.path) .file_stem() .and_then(|stem| stem.to_str()) .filter(|label| !label.trim().is_empty()) .unwrap_or("Skinned mesh"); let thumbnail = source_candidate.and_then(|candidate| candidate.texture_id); renderer_panel(ui, |ui| { renderer_panel_header( ui, RendererPanelHeaderViewModel { index: 0, label, path: if renderer.path.trim().is_empty() { "No model source assigned" } else { renderer.path.as_str() }, slot_count: renderer.materials.slots.len(), texture_id: thumbnail, }, &mut renderer_open, |ui| { if editor_ui::design_system::controls::icon_button( ui, icons::CROSSHAIR, "Locate model", source_candidate.is_some(), ) .clicked() { locate_mesh = true; } if editor_ui::design_system::controls::icon_button( ui, icons::FOLDER_OPEN, "Browse models", true, ) .clicked() { browse_mesh = true; } }, ); if !renderer_open { return; } egui::Frame::new() .inner_margin(egui::Margin::symmetric(12, 10)) .show(ui, |ui| { renderer_properties_header(ui); property_row(ui, "Source", |ui| { ui.add( egui::Label::new(if renderer.path.trim().is_empty() { "Unassigned" } else { renderer.path.as_str() }) .truncate(), ); }); property_row(ui, "Scene", |ui| { ui.label(renderer.scene_index.to_string()); }); property_row(ui, "Asset ID", |ui| { ui.add( egui::Label::new(if renderer.asset_id.trim().is_empty() { "Unresolved" } else { renderer.asset_id.as_str() }) .truncate(), ); }); ui.small( egui::RichText::new( "Imported skeleton hierarchy and skin bindings remain model-owned.", ) .color(TEXT_DIM), ); }); renderer_material_slots_header(ui, renderer.materials.slots.len()); if renderer.materials.slots.is_empty() { egui::Frame::new() .inner_margin(egui::Margin::symmetric(12, 10)) .show(ui, |ui| { ui.small( egui::RichText::new("No imported slots; reimport the source model") .color(TEXT_DIM), ); }); } for (index, slot) in renderer.materials.slots.iter_mut().enumerate() { let widget = renderer_material_slot_row(ui, index, |ui| { material_slot_widget_ui( world, ui, entity, slot, MaterialSlotWidgetContext { candidates: &material_candidates, drop_candidate: material_drop_candidate.as_ref(), invalid_drop_reason: invalid_material_drop, expanded_header_when_parameters_closed: true, }, ) }); changed |= widget.changed; accepted_drop |= widget.accepted_drop; } }); ui.ctx() .data_mut(|data| data.insert_persisted(renderer_open_id, renderer_open)); for orphan in &renderer.materials.orphaned_assignments { ui.label( egui::RichText::new(format!( "Orphaned: {} ({})", orphan.last_known_name, orphan.slot_id.0 )) .small() .color(egui::Color32::YELLOW), ); } if world.get::(entity).is_some() { ui.label( egui::RichText::new( "Remove the Animation Controller before removing its renderer.", ) .small() .color(TEXT_MUTED), ); } }); apply_component_card_response(world, entity, response); if browse_mesh { crate::ui::request_editor_tab(world, crate::ui::EditorTab::AssetBrowser); } if locate_mesh { if let Some(candidate) = mesh_candidates .iter() .find(|candidate| candidate.reference.asset_id == renderer.asset_id) { reveal_asset_in_browser(world, &candidate.folder_path, &candidate.selection); } } if accepted_drop { clear_asset_drag(world); } if changed && renderer != original { let result = reflected_component_transaction( world, entity, "Assign Skinned Material Slot", shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER, COMPONENT_SKINNED_MESH_RENDERER, move |world, entity| { world.entity_mut(entity).insert(renderer); Ok(()) }, ); if let Err(error) = result { world.resource_mut::().status = error; } } } pub(super) fn static_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let mesh_candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Mesh); let material_candidates = brush_face_material_ref_candidates(world); let material_drop_candidate = world .get_resource::() .and_then(|assets| assets.dragging_selection().cloned()) .and_then(|selection| { asset_ref_candidate_from_selection(world, &selection, AssetRefCandidateKind::Material) }); let invalid_material_drop = material_drop_invalid_reason(world, material_drop_candidate.is_some()); let Some(mut renderer) = world.get::(entity).cloned() else { return; }; let original = renderer.clone(); let mut changed = false; let mut accepted_drop = false; let mut remove_entry = None; let mut browse_mesh = false; let mut locate_mesh = None; for index in 0..renderer.slots.len() { ensure_slot_id(&mut renderer.slots[index], index); let part = &renderer.slots[index]; if renderer.materials.slot(&part.material_slot_id).is_none() { renderer.materials.slots.push(shared::MaterialSlot { id: part.material_slot_id.clone(), name: part.name.clone(), material: None, }); } } let mut options = ComponentCardOptions::removable( COMPONENT_STATIC_MESH_RENDERER, "Static Mesh Renderer", icons::CUBE, ); options.summary = "Renderer array"; options.body_margin = 0; let card = component_card_context(world, entity, options); let card_response = component_card(ui, &card, |ui| { let add_slot = renderer_array_header(ui, renderer.slots.len()); ui.add_space(26.0); if renderer.slots.is_empty() { ui.label(egui::RichText::new("No renderer slots").color(TEXT_DIM)); } let (entries, material_set) = (&mut renderer.slots, &mut renderer.materials); for (index, entry) in entries.iter_mut().enumerate() { ensure_slot_id(entry, index); let thumbnail = thumbnail_for_mesh(&entry.mesh, &mesh_candidates); let renderer_open_id = ui.make_persistent_id(("static_renderer_open", &entry.id.0)); let mut renderer_open = ui .ctx() .data_mut(|data| data.get_persisted::(renderer_open_id)) .unwrap_or(index == 0); renderer_panel(ui, |ui| { renderer_panel_header( ui, RendererPanelHeaderViewModel { index, label: &entry.name, path: entry .mesh .source_path .as_deref() .unwrap_or(&entry.mesh.sub_asset_id), slot_count: 1, texture_id: thumbnail, }, &mut renderer_open, |ui| { editor_ui::design_system::controls::icon_button( ui, icons::DOTS_THREE_VERTICAL, "Renderer actions", true, ); if editor_ui::design_system::controls::icon_button( ui, icons::TRASH, "Remove slot", true, ) .clicked() { remove_entry = Some(index); } if editor_ui::design_system::controls::icon_button( ui, icons::CROSSHAIR, "Locate mesh", true, ) .clicked() { locate_mesh = Some(entry.mesh.clone()); } if editor_ui::design_system::controls::icon_button( ui, icons::FOLDER_OPEN, "Browse mesh", true, ) .clicked() { browse_mesh = true; } }, ); if !renderer_open { return; } egui::Frame::new() .inner_margin(egui::Margin::symmetric(12, 10)) .show(ui, |ui| { renderer_properties_header(ui); ui.add_space(2.0); property_row(ui, "Name", |ui| { changed |= ui .add_sized( [text_field_width(ui), 20.0], egui::TextEdit::singleline(&mut entry.name), ) .changed(); }); ui.add_space(4.0); let mesh_response = asset_selector_row( ui, "Mesh", icons::CUBE, Some(&entry.mesh), None, false, &mesh_candidates, None, None, AssetSelectorActions::Full, ); if let Some(selected) = mesh_response.selected { entry.mesh = selected; changed = true; } if mesh_response.locate { locate_asset_ref(world, Some(&entry.mesh), &mesh_candidates); } ui.add_space(4.0); ui.horizontal_wrapped(|ui| { changed |= ui.checkbox(&mut entry.visible, "Visible").changed(); changed |= ui .checkbox(&mut entry.cast_shadows, "Cast shadows") .changed(); changed |= ui .checkbox(&mut entry.receive_shadows, "Receive shadows") .changed(); }); ui.add_space(4.0); }); renderer_material_slots_header(ui, 1); if let Some(slot) = material_set.slot_mut(&entry.material_slot_id) { let widget = renderer_material_slot_row(ui, index, |ui| { material_slot_widget_ui( world, ui, entity, slot, MaterialSlotWidgetContext { candidates: &material_candidates, drop_candidate: material_drop_candidate.as_ref(), invalid_drop_reason: invalid_material_drop, expanded_header_when_parameters_closed: true, }, ) }); changed |= widget.changed; accepted_drop |= widget.accepted_drop; } }); ui.ctx() .data_mut(|data| data.insert_persisted(renderer_open_id, renderer_open)); } if add_slot { let index = renderer.slots.len(); let entry = StaticMeshRendererEntry { id: ComponentInstanceId::new(format!("slot:{index}")), material_slot_id: ComponentInstanceId::new(format!("slot:manual:{index}")), ..Default::default() }; renderer.materials.slots.push(shared::MaterialSlot { id: entry.material_slot_id.clone(), name: format!("Material {index}"), material: None, }); renderer.slots.push(entry); changed = true; } }); apply_component_card_response(world, entity, card_response); if browse_mesh { crate::ui::request_editor_tab(world, crate::ui::EditorTab::AssetBrowser); } if let Some(mesh) = locate_mesh.as_ref() { locate_asset_ref(world, Some(mesh), &mesh_candidates); } if accepted_drop { clear_asset_drag(world); } if let Some(index) = remove_entry { let removed = renderer.slots.remove(index); if let Some(slot_index) = renderer .materials .slots .iter() .position(|slot| slot.id == removed.material_slot_id) { let slot = renderer.materials.slots.remove(slot_index); if let Some(material) = slot.material { renderer .materials .orphaned_assignments .push(shared::OrphanedMaterialAssignment { slot_id: slot.id, last_known_name: slot.name, material, }); } } changed = true; } if changed && renderer != original { set_static_mesh_renderer_with_history(world, entity, renderer); } }