Blacksite/crates/editor/src/ui/inspector/primitive_domains.rs
Rbanh 53dc1e44d8
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
feat: add production UI gallery and inspector system
2026-07-18 12:05:26 -04:00

616 lines
23 KiB
Rust

use super::*;
pub(super) fn clear_asset_drag(world: &mut World) {
if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
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::<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 primitive) = world.get::<Primitive>(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::<LightDesc>(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::<settings::ActiveCameraRenderProfile>()
.is_some_and(|profile| profile.gi_path == settings::GiPath::SolariDeferred)
&& world
.get_resource::<settings::RenderingCapabilities>()
.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::<RigidBodyDesc>(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::<ColliderDesc>(entity).cloned() else {
return;
};
let original = collider.clone();
let mut changed = false;
let renderer_meshes: Vec<EditorAssetRef> = world
.get::<StaticMeshRenderer>(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<EditorAssetRef>,
) -> 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::<PhysicsBody>(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::<PlayerSpawn>(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::<WeaponSpawn>(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::<TriggerVolume>(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::<TeamSpawn>(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::<ObjectiveMarker>(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);
}