446 lines
18 KiB
Rust
446 lines
18 KiB
Rust
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::<EditorAssets>()
|
|
.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::<SkinnedMeshRenderer>(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::<AnimationControllerDesc>(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::<bool>(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::<AnimationControllerDesc>(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::<crate::scene_io::SceneIo>().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::<EditorAssets>()
|
|
.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::<StaticMeshRenderer>(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::<bool>(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);
|
|
}
|
|
}
|