use super::*; pub(super) fn clear_asset_drag(world: &mut World) { if let Some(mut assets) = world.get_resource_mut::() { assets.clear_drag(); } } pub(super) fn primitive_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { 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 primitive) = world.get::(entity).cloned() else { return; }; let original = primitive.clone(); let mut changed = false; let mut accepted_drop = false; let mut options = ComponentCardOptions::removable(COMPONENT_PRIMITIVE, "Primitive", icons::CUBE); options.summary = "Shape · Size · Materials"; let card = component_card_context(world, entity, options); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Shape", |ui| { ui.horizontal_wrapped(|ui| { for (label, shape) in [ ("Box", PrimitiveShape::Box), ("Sphere", PrimitiveShape::Sphere), ("Ramp", PrimitiveShape::Ramp), ] { changed |= ui .selectable_value(&mut primitive.shape, shape, label) .changed(); } }); }); property_row(ui, "Size X", |ui| { changed |= ui .add(egui::Slider::new(&mut primitive.size.x, 0.1..=1000.0)) .changed(); }); property_row(ui, "Size Y", |ui| { changed |= ui .add(egui::Slider::new(&mut primitive.size.y, 0.1..=1000.0)) .changed(); }); property_row(ui, "Size Z", |ui| { changed |= ui .add(egui::Slider::new(&mut primitive.size.z, 0.1..=1000.0)) .changed(); }); ui.add_space(8.0); let materials = crate::ui::materials::MaterialsSectionViewModel { slot_ids: vec![primitive.surface.id.0.clone()], }; let widget = crate::ui::materials::materials_section(ui, &materials, |ui| { material_slot_widget_ui( world, ui, entity, &mut primitive.surface, MaterialSlotWidgetContext { candidates: &material_candidates, drop_candidate: material_drop_candidate.as_ref(), invalid_drop_reason: invalid_material_drop, expanded_header_when_parameters_closed: false, }, ) }); changed |= widget.changed; accepted_drop |= widget.accepted_drop; }); apply_component_card_response(world, entity, card_response); if accepted_drop { clear_asset_drag(world); } if changed && primitive != original { set_primitive_with_history(world, entity, primitive); } } pub(super) fn light_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut light) = world.get::(entity).cloned() else { return; }; let _original = light.clone(); let mut changed = false; let card = component_card_context( world, entity, ComponentCardOptions::removable(COMPONENT_LIGHT_DESC, "Light", icons::LIGHTBULB), ); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Kind", |ui| { ui.horizontal_wrapped(|ui| { for (label, kind) in [ ("Point", AuthoringLightKind::Point), ("Spot", AuthoringLightKind::Spot), ("Directional", AuthoringLightKind::Directional), ] { if ui.selectable_label(light.kind == kind, label).clicked() { let color = light.color; light = LightDesc { color, ..LightDesc::for_kind(kind) }; changed = true; } } }); }); let solari_disabled = solari_disables_local_light(world, &light); if solari_disabled { ui.colored_label( egui::Color32::from_rgb(255, 180, 100), "Point and spot lights are disabled while Solari is active. Use a directional light or emissive material, or switch GI to Forward.", ); } ui.add_enabled_ui(!solari_disabled, |ui| { let mut color = [light.color.r, light.color.g, light.color.b, light.color.a]; property_row(ui, "Color", |ui| { if ui.color_edit_button_rgba_unmultiplied(&mut color).changed() { light.color = ColorDesc { r: color[0], g: color[1], b: color[2], a: color[3], }; changed = true; } }); let intensity_label = match light.kind { AuthoringLightKind::Directional => "Intensity (lux)", _ => "Intensity (lumens)", }; let intensity_max = match light.kind { AuthoringLightKind::Directional => AUTHORING_DIRECTIONAL_LUX_MAX, _ => AUTHORING_POINT_SPOT_LUMENS_MAX, }; property_row(ui, intensity_label, |ui| { ui.horizontal_wrapped(|ui| { if ui .add( egui::Slider::new(&mut light.intensity, 0.0..=intensity_max) .show_value(false), ) .changed() { changed = true; } if ui .add_sized( [fit_width(ui, 72.0, 120.0), 20.0], egui::DragValue::new(&mut light.intensity) .range(0.0..=intensity_max) .speed(intensity_max * 0.001), ) .changed() { changed = true; } }); }); if matches!( light.kind, AuthoringLightKind::Point | AuthoringLightKind::Spot ) { property_row(ui, "Range (m)", |ui| { changed |= ui .add(egui::Slider::new(&mut light.range, 0.0..=100.0)) .changed(); }); } if matches!(light.kind, AuthoringLightKind::Spot) { property_row(ui, "Inner angle", |ui| { changed |= ui .add(egui::Slider::new(&mut light.inner_angle_deg, 1.0..=80.0)) .changed(); }); property_row(ui, "Outer angle", |ui| { changed |= ui .add(egui::Slider::new(&mut light.outer_angle_deg, 1.0..=90.0)) .changed(); }); } property_row(ui, "Shadows", |ui| { changed |= ui.checkbox(&mut light.shadows, "Cast shadows").changed(); }); if matches!(light.kind, AuthoringLightKind::Directional) { ui.small("Controls project sun while this directional exists."); } }); }); apply_component_card_response(world, entity, card_response); if changed { set_light_with_history(world, entity, light); } } pub(super) fn solari_disables_local_light(world: &World, light: &LightDesc) -> bool { matches!( light.kind, AuthoringLightKind::Point | AuthoringLightKind::Spot ) && world .get_resource::() .is_some_and(|profile| profile.gi_path == settings::GiPath::SolariDeferred) && world .get_resource::() .is_some_and(|caps| caps.rt_supported) } pub(super) fn rigid_body_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut body) = world.get::(entity).copied() else { return; }; let original = body; let mut changed = false; let card = component_card_context( world, entity, ComponentCardOptions::removable(COMPONENT_RIGID_BODY_DESC, "Rigid Body", icons::SPHERE), ); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Body", |ui| { ui.horizontal_wrapped(|ui| { for (label, kind) in [ ("Static", AuthoringRigidBody::Static), ("Kinematic", AuthoringRigidBody::Kinematic), ("Dynamic", AuthoringRigidBody::Dynamic), ] { changed |= ui.selectable_value(&mut body.body, kind, label).changed(); } }); }); }); apply_component_card_response(world, entity, card_response); if changed && body != original { set_rigid_body_with_history(world, entity, body); } } pub(super) fn collider_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let mesh_candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Mesh); let diagnostic_entry = diagnose_collider(world, entity); let Some(mut collider) = world.get::(entity).cloned() else { return; }; let original = collider.clone(); let mut changed = false; let renderer_meshes: Vec = world .get::(entity) .map(|renderer| { renderer .slots .iter() .map(|slot| slot.mesh.clone()) .collect() }) .unwrap_or_default(); let card = component_card_context( world, entity, ComponentCardOptions::removable(COMPONENT_COLLIDER_DESC, "Collider", icons::SELECTION), ); let card_response = component_card(ui, &card, |ui| { if let Some(entry) = diagnostic_entry.as_ref() { let (label, color) = match entry.overlay_status { ColliderOverlayStatus::Valid => ("Ready", egui::Color32::from_rgb(112, 210, 144)), ColliderOverlayStatus::Trigger => { ("Trigger", egui::Color32::from_rgb(86, 195, 235)) } ColliderOverlayStatus::Disabled => { ("Disabled", egui::Color32::from_rgb(143, 151, 163)) } ColliderOverlayStatus::Warning => { ("Warning", egui::Color32::from_rgb(242, 173, 72)) } ColliderOverlayStatus::Error => ("Invalid", egui::Color32::from_rgb(244, 91, 99)), }; ui.horizontal_wrapped(|ui| { ui.colored_label(color, egui::RichText::new(label).strong()); ui.label(egui::RichText::new(entry.shape_label).color(TEXT_DIM)); if entry.runtime_ready { ui.label(egui::RichText::new("Hydrated").color(TEXT_MUTED).small()); } }); for diagnostic in &entry.diagnostics { let color = match diagnostic.severity { ColliderDiagnosticSeverity::Info => TEXT_MUTED, ColliderDiagnosticSeverity::Warning => egui::Color32::from_rgb(242, 173, 72), ColliderDiagnosticSeverity::Error => egui::Color32::from_rgb(244, 91, 99), }; ui.label( egui::RichText::new(&diagnostic.message) .color(color) .small(), ); ui.label( egui::RichText::new(&diagnostic.repair) .color(TEXT_MUTED) .small(), ); } if entry.highest_severity() == Some(ColliderDiagnosticSeverity::Error) && ui.small_button("Reset shape").clicked() { collider.shape = ColliderShapeDesc::default(); collider.enabled = true; changed = true; } ui.add_space(2.0); } property_row(ui, "Mode", |ui| { changed |= ui.checkbox(&mut collider.enabled, "Enabled").changed(); changed |= ui.checkbox(&mut collider.is_trigger, "Trigger").changed(); }); property_row(ui, "Shape", |ui| { let mut shape_kind = collider_shape_kind(&collider.shape); egui::ComboBox::from_id_salt(("collider_shape_kind", entity)) .selected_text(shape_kind) .show_ui(ui, |ui| { for label in ["Box", "Sphere", "Capsule", "Static Mesh"] { if ui.selectable_label(shape_kind == label, label).clicked() { shape_kind = label; } } }); if shape_kind != collider_shape_kind(&collider.shape) { collider.shape = convert_collider_shape(&collider.shape, shape_kind, renderer_meshes.clone()); changed = true; } }); match &mut collider.shape { ColliderShapeDesc::Cuboid { x_length, y_length, z_length, } => { changed |= dimension_drag(ui, "X", x_length); changed |= dimension_drag(ui, "Y", y_length); changed |= dimension_drag(ui, "Z", z_length); } ColliderShapeDesc::Sphere { radius } => { changed |= dimension_drag(ui, "Radius", radius); } ColliderShapeDesc::Capsule { radius, height } => { changed |= dimension_drag(ui, "Radius", radius); changed |= dimension_drag(ui, "Height", height); } ColliderShapeDesc::StaticMesh { meshes, .. } => { if meshes.is_empty() { ui.label(egui::RichText::new("No mesh collider sources").color(TEXT_DIM)); } for mesh in meshes.iter_mut() { let mesh_response = asset_selector_row( ui, "Mesh", icons::CUBE, Some(mesh), None, false, &mesh_candidates, None, None, AssetSelectorActions::Full, ); if let Some(selected) = mesh_response.selected { *mesh = selected; changed = true; } if mesh_response.locate { locate_asset_ref(world, Some(mesh), &mesh_candidates); } } if !renderer_meshes.is_empty() && ui.button("Use renderer meshes").clicked() { *meshes = renderer_meshes.clone(); changed = true; } } } }); apply_component_card_response(world, entity, card_response); if changed && collider != original { set_collider_with_history(world, entity, collider); } } pub(super) fn collider_shape_kind(shape: &ColliderShapeDesc) -> &'static str { match shape { ColliderShapeDesc::Cuboid { .. } => "Box", ColliderShapeDesc::Sphere { .. } => "Sphere", ColliderShapeDesc::Capsule { .. } => "Capsule", ColliderShapeDesc::StaticMesh { .. } => "Static Mesh", } } pub(super) fn convert_collider_shape( previous: &ColliderShapeDesc, shape_kind: &str, renderer_meshes: Vec, ) -> ColliderShapeDesc { let dimensions = match previous { ColliderShapeDesc::Cuboid { x_length, y_length, z_length, } => Vec3::new(*x_length, *y_length, *z_length), ColliderShapeDesc::Sphere { radius } => Vec3::splat(*radius * 2.0), ColliderShapeDesc::Capsule { radius, height } => { Vec3::new(*radius * 2.0, *height, *radius * 2.0) } ColliderShapeDesc::StaticMesh { .. } => Vec3::ONE, } .max(Vec3::splat(0.001)); match shape_kind { "Sphere" => ColliderShapeDesc::Sphere { radius: dimensions.max_element() * 0.5, }, "Capsule" => ColliderShapeDesc::Capsule { radius: dimensions.x.max(dimensions.z) * 0.5, height: dimensions.y, }, "Static Mesh" => ColliderShapeDesc::static_mesh(renderer_meshes), _ => ColliderShapeDesc::Cuboid { x_length: dimensions.x, y_length: dimensions.y, z_length: dimensions.z, }, } } pub(super) fn dimension_drag(ui: &mut egui::Ui, label: &str, value: &mut f32) -> bool { property_row(ui, label, |ui| { ui.add_sized( [fit_width(ui, 72.0, 120.0), 20.0], egui::DragValue::new(value) .range(0.001..=10_000.0) .speed(0.05) .min_decimals(2) .max_decimals(3), ) .changed() }) } pub(super) fn physics_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut body) = world.get::(entity).cloned() else { return; }; let _original = body.clone(); let mut changed = false; let card = component_card_context( world, entity, ComponentCardOptions::removable(COMPONENT_PHYSICS_BODY, "Physics", icons::SPHERE), ); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Body", |ui| { ui.horizontal_wrapped(|ui| { for (label, kind) in [ ("Static", AuthoringRigidBody::Static), ("Kinematic", AuthoringRigidBody::Kinematic), ("Dynamic", AuthoringRigidBody::Dynamic), ] { if ui.selectable_label(body.body == kind, label).clicked() { body.body = kind; changed = true; } } }); }); }); apply_component_card_response(world, entity, card_response); if changed { set_physics_with_history(world, entity, body); } } pub(super) fn player_spawn_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { if world.get::(entity).is_none() { return; } let card = component_card_context( world, entity, ComponentCardOptions::removable( COMPONENT_PLAYER_SPAWN, "Player Spawn", icons::PERSON_SIMPLE_RUN, ), ); let card_response = component_card(ui, &card, |ui| { ui.label( egui::RichText::new("Uses this actor transform as a player start.").color(TEXT_DIM), ); }); apply_component_card_response(world, entity, card_response); } pub(super) fn weapon_spawn_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut spawn) = world.get::(entity).cloned() else { return; }; let card = component_card_context( world, entity, ComponentCardOptions::removable(COMPONENT_WEAPON_SPAWN, "Weapon Spawn", icons::CROSSHAIR), ); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Weapon ID", |ui| { ui.add_sized( [text_field_width(ui), 20.0], egui::TextEdit::singleline(&mut spawn.weapon_id), ); }); }); apply_component_card_response(world, entity, card_response); if let Ok(mut e) = world.get_entity_mut(entity) { e.insert(spawn); } } pub(super) fn trigger_volume_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut trigger) = world.get::(entity).cloned() else { return; }; let card = component_card_context( world, entity, ComponentCardOptions::removable( COMPONENT_TRIGGER_VOLUME, "Trigger Volume", icons::SELECTION, ), ); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Event", |ui| { ui.add_sized( [text_field_width(ui), 20.0], egui::TextEdit::singleline(&mut trigger.event_name), ); }); property_row(ui, "Half X", |ui| { ui.add(egui::Slider::new(&mut trigger.half_extents.x, 0.1..=50.0)); }); property_row(ui, "Half Y", |ui| { ui.add(egui::Slider::new(&mut trigger.half_extents.y, 0.1..=50.0)); }); property_row(ui, "Half Z", |ui| { ui.add(egui::Slider::new(&mut trigger.half_extents.z, 0.1..=50.0)); }); }); apply_component_card_response(world, entity, card_response); if let Ok(mut e) = world.get_entity_mut(entity) { e.insert(trigger); } } pub(super) fn team_spawn_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut spawn) = world.get::(entity).cloned() else { return; }; let card = component_card_context( world, entity, ComponentCardOptions::removable(COMPONENT_TEAM_SPAWN, "Team Spawn", icons::FLAG), ); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Team", |ui| { ui.add(egui::Slider::new(&mut spawn.team_id, 0..=8)); }); }); apply_component_card_response(world, entity, card_response); if let Ok(mut e) = world.get_entity_mut(entity) { e.insert(spawn); } } pub(super) fn objective_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut marker) = world.get::(entity).cloned() else { return; }; let card = component_card_context( world, entity, ComponentCardOptions::removable(COMPONENT_OBJECTIVE_MARKER, "Objective", icons::TARGET), ); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Objective ID", |ui| { ui.add_sized( [text_field_width(ui), 20.0], egui::TextEdit::singleline(&mut marker.objective_id), ); }); }); apply_component_card_response(world, entity, card_response); if let Ok(mut e) = world.get_entity_mut(entity) { e.insert(marker); } } pub(super) fn prefab_instance_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { crate::assets::prefab_overrides::prefab_instance_inspector_ui(world, ui, entity); }