feat(editor): implement smart entity naming system for improved display names
This commit is contained in:
parent
9e889396db
commit
9993b202e0
@ -36,7 +36,7 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::remote::{client, RemoteClientPlugin, types::{EditorState, ComponentDisplayState, ComponentDataFetched, RemoteConnection, EntitiesFetched, ConnectionStatus, RemoteEntity}};
|
||||
use crate::remote::{client, RemoteClientPlugin, types::{EditorState, ComponentDisplayState, ComponentDataFetched, RemoteConnection, EntitiesFetched, ConnectionStatus}};
|
||||
use crate::panels::{EntityListPlugin, ComponentInspectorPlugin};
|
||||
use crate::widgets::{WidgetsPlugin, ScrollViewBuilder, ScrollContent};
|
||||
|
||||
@ -71,7 +71,6 @@ impl Plugin for EditorPlugin {
|
||||
.add_systems(Startup, setup_editor_ui)
|
||||
.add_systems(Update, (
|
||||
setup_scroll_content_markers,
|
||||
refresh_entity_list,
|
||||
handle_entity_selection,
|
||||
update_entity_button_colors,
|
||||
handle_component_inspection,
|
||||
@ -433,102 +432,7 @@ fn update_remote_connection(
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh the entity list display
|
||||
fn refresh_entity_list(
|
||||
editor_state: Res<EditorState>,
|
||||
mut commands: Commands,
|
||||
entity_list_area_query: Query<Entity, With<EntityListArea>>,
|
||||
list_items_query: Query<Entity, With<EntityListItem>>,
|
||||
mut local_entity_count: Local<usize>,
|
||||
) {
|
||||
// Only refresh when the actual entity count changes, not on every state change
|
||||
let current_count = editor_state.entities.len();
|
||||
if *local_entity_count == current_count {
|
||||
return;
|
||||
}
|
||||
*local_entity_count = current_count;
|
||||
|
||||
// Clear existing list items
|
||||
for entity in &list_items_query {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
|
||||
// Find the entity list area and add new items
|
||||
for list_area_entity in entity_list_area_query.iter() {
|
||||
// Clear children by despawning them
|
||||
commands.entity(list_area_entity).despawn_children();
|
||||
|
||||
commands.entity(list_area_entity).with_children(|parent| {
|
||||
if editor_state.entities.is_empty() {
|
||||
// Show empty state
|
||||
parent.spawn((
|
||||
Text::new("No entities connected.\nStart a bevy_remote server to see entities."),
|
||||
TextFont {
|
||||
font_size: 12.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.6, 0.6, 0.6)),
|
||||
Node {
|
||||
padding: UiRect::all(Val::Px(16.0)),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
} else {
|
||||
// Add entity items
|
||||
for remote_entity in &editor_state.entities {
|
||||
create_entity_list_item(parent, remote_entity, &editor_state);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn create_entity_list_item(parent: &mut ChildSpawnerCommands, remote_entity: &RemoteEntity, editor_state: &EditorState) {
|
||||
// Determine the correct background color based on selection state
|
||||
let bg_color = if Some(remote_entity.id) == editor_state.selected_entity_id {
|
||||
Color::srgb(0.3, 0.4, 0.5) // Selected state
|
||||
} else {
|
||||
Color::srgb(0.2, 0.2, 0.2) // Default state
|
||||
};
|
||||
|
||||
parent
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Px(32.0),
|
||||
align_items: AlignItems::Center,
|
||||
padding: UiRect::all(Val::Px(10.0)),
|
||||
margin: UiRect::bottom(Val::Px(2.0)),
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(bg_color),
|
||||
BorderColor::all(Color::srgb(0.3, 0.3, 0.3)),
|
||||
EntityListItem::from_remote_entity(&remote_entity),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
// Entity icon and name
|
||||
parent.spawn((
|
||||
Node {
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
)).with_children(|parent| {
|
||||
parent.spawn((
|
||||
Text::new(format!("Entity {}", remote_entity.id)),
|
||||
TextFont {
|
||||
font_size: 13.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Create status bar with connection info
|
||||
/// Update status bar with connection info
|
||||
fn update_status_bar(
|
||||
editor_state: Res<EditorState>,
|
||||
remote_conn: Res<RemoteConnection>,
|
||||
|
314
crates/bevy_editor/src/remote/entity_naming.rs
Normal file
314
crates/bevy_editor/src/remote/entity_naming.rs
Normal file
@ -0,0 +1,314 @@
|
||||
//! Smart entity naming system for the editor
|
||||
//!
|
||||
//! This module provides intelligent naming for entities based on their components.
|
||||
//! It follows a precedence system:
|
||||
//! 1. Name component (highest priority)
|
||||
//! 2. Common Bevy components (Camera, Window, etc.)
|
||||
//! 3. User components
|
||||
//! 4. Entity ID fallback
|
||||
|
||||
use super::types::RemoteEntity;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Component precedence levels for naming
|
||||
#[derive(Debug, PartialEq, PartialOrd, Clone)]
|
||||
pub enum ComponentPrecedence {
|
||||
/// Built-in Name component - highest priority
|
||||
Name = 0,
|
||||
/// User/custom components - very high priority for meaningful names
|
||||
UserComponent = 1,
|
||||
/// Primary Bevy components (Camera, Window, etc.)
|
||||
PrimaryBevy = 2,
|
||||
/// Secondary Bevy components (Transform, Visibility, etc.)
|
||||
SecondaryBevy = 3,
|
||||
/// Entity ID fallback - lowest priority
|
||||
EntityId = 4,
|
||||
}
|
||||
|
||||
/// A component that can provide a name for an entity
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NamingComponent {
|
||||
pub name: String,
|
||||
pub precedence: ComponentPrecedence,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
/// Generate a smart display name for an entity based on its components
|
||||
pub fn generate_entity_display_name(entity: &RemoteEntity, component_data: Option<&Value>) -> String {
|
||||
let mut best_naming: Option<NamingComponent> = None;
|
||||
|
||||
// First check if we have component data with actual Name values
|
||||
if let Some(data) = component_data {
|
||||
if let Some(name_value) = extract_name_from_component_data(data) {
|
||||
return format!("#{} ({})", entity.id, name_value);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze available components to find the best naming option
|
||||
for component_name in &entity.components {
|
||||
if let Some(naming) = analyze_component_for_naming(component_name) {
|
||||
if best_naming.is_none() || naming.precedence < best_naming.as_ref().unwrap().precedence {
|
||||
best_naming = Some(naming);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check full component names for more accurate detection
|
||||
for full_component_name in &entity.full_component_names {
|
||||
if let Some(naming) = analyze_component_for_naming(full_component_name) {
|
||||
if best_naming.is_none() || naming.precedence < best_naming.as_ref().unwrap().precedence {
|
||||
best_naming = Some(naming);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match best_naming {
|
||||
Some(naming) if naming.precedence != ComponentPrecedence::EntityId => {
|
||||
format!("#{} ({})", entity.id, naming.display_name)
|
||||
}
|
||||
_ => {
|
||||
// Fallback to entity ID
|
||||
format!("Entity {}", entity.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the actual name value from component data JSON
|
||||
fn extract_name_from_component_data(component_data: &Value) -> Option<String> {
|
||||
if let Some(components) = component_data.get("components").and_then(|v| v.as_object()) {
|
||||
// Look for Name component in various forms
|
||||
for (component_type, component_value) in components {
|
||||
if component_type.contains("Name") || component_type.ends_with("::Name") {
|
||||
// Try to extract the actual name value
|
||||
if let Some(name_str) = component_value.as_str() {
|
||||
return Some(name_str.to_string());
|
||||
}
|
||||
// Handle nested name values
|
||||
if let Some(obj) = component_value.as_object() {
|
||||
if let Some(name_val) = obj.get("name").or_else(|| obj.get("value")).or_else(|| obj.get("0")) {
|
||||
if let Some(name_str) = name_val.as_str() {
|
||||
return Some(name_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Analyze a component name to determine if it can provide entity naming
|
||||
fn analyze_component_for_naming(component_name: &str) -> Option<NamingComponent> {
|
||||
// Remove module paths for analysis
|
||||
let clean_name = component_name
|
||||
.split("::")
|
||||
.last()
|
||||
.unwrap_or(component_name);
|
||||
|
||||
// Check for Name component (highest priority)
|
||||
if clean_name == "Name" || component_name.ends_with("::Name") {
|
||||
return Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::Name,
|
||||
display_name: "Named Entity".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Primary Bevy components that are very distinctive
|
||||
match clean_name {
|
||||
"Camera" | "Camera2d" | "Camera3d" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::PrimaryBevy,
|
||||
display_name: "Camera".to_string(),
|
||||
}),
|
||||
"Window" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::PrimaryBevy,
|
||||
display_name: "Window".to_string(),
|
||||
}),
|
||||
"DirectionalLight" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::PrimaryBevy,
|
||||
display_name: "Directional Light".to_string(),
|
||||
}),
|
||||
"PointLight" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::PrimaryBevy,
|
||||
display_name: "Point Light".to_string(),
|
||||
}),
|
||||
"SpotLight" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::PrimaryBevy,
|
||||
display_name: "Spot Light".to_string(),
|
||||
}),
|
||||
"AudioListener" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::PrimaryBevy,
|
||||
display_name: "Audio Listener".to_string(),
|
||||
}),
|
||||
"AudioSource" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::PrimaryBevy,
|
||||
display_name: "Audio Source".to_string(),
|
||||
}),
|
||||
|
||||
// Secondary Bevy components (lower priority)
|
||||
"Mesh3d" | "Mesh2d" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::SecondaryBevy,
|
||||
display_name: "Mesh".to_string(),
|
||||
}),
|
||||
"MeshMaterial3d" | "MeshMaterial2d" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::SecondaryBevy,
|
||||
display_name: "Material".to_string(),
|
||||
}),
|
||||
"Text" | "Text2d" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::SecondaryBevy,
|
||||
display_name: "Text".to_string(),
|
||||
}),
|
||||
"Sprite" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::SecondaryBevy,
|
||||
display_name: "Sprite".to_string(),
|
||||
}),
|
||||
"ImageNode" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::SecondaryBevy,
|
||||
display_name: "Image".to_string(),
|
||||
}),
|
||||
"Button" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::SecondaryBevy,
|
||||
display_name: "Button".to_string(),
|
||||
}),
|
||||
"Node" => Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::SecondaryBevy,
|
||||
display_name: "UI Node".to_string(),
|
||||
}),
|
||||
|
||||
_ => {
|
||||
// Check if it's likely a user component (not starting with common Bevy prefixes)
|
||||
if !is_bevy_builtin_component(component_name) {
|
||||
Some(NamingComponent {
|
||||
name: component_name.to_string(),
|
||||
precedence: ComponentPrecedence::UserComponent,
|
||||
display_name: clean_name.to_string(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a component is likely a Bevy built-in component
|
||||
fn is_bevy_builtin_component(component_name: &str) -> bool {
|
||||
let bevy_prefixes = [
|
||||
"bevy_",
|
||||
"std::",
|
||||
"core::",
|
||||
"alloc::",
|
||||
"winit::",
|
||||
];
|
||||
|
||||
let has_bevy_prefix = bevy_prefixes.iter().any(|prefix| component_name.starts_with(prefix));
|
||||
let is_common_bevy = matches!(component_name.split("::").last().unwrap_or(""),
|
||||
"Transform" | "GlobalTransform" | "Visibility" | "InheritedVisibility" |
|
||||
"ViewVisibility" | "ComputedVisibility" | "Parent" | "Children" | "Aabb" |
|
||||
"TransformTreeChanged" | "Frustum" | "Projection" | "VisibleEntities" |
|
||||
"DebandDither" | "Tonemapping" | "ClusterConfig" | "RenderEntity" |
|
||||
"SyncToRenderWorld" | "Msaa" | "CubemapFrusta" | "CubemapVisibleEntities"
|
||||
);
|
||||
|
||||
has_bevy_prefix || is_common_bevy
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_entity_naming_precedence() {
|
||||
let mut entity = RemoteEntity {
|
||||
id: 123,
|
||||
components: vec!["Camera".to_string(), "Transform".to_string()],
|
||||
full_component_names: vec!["bevy_render::camera::Camera".to_string(), "bevy_transform::components::Transform".to_string()],
|
||||
};
|
||||
|
||||
let display_name = generate_entity_display_name(&entity, None);
|
||||
assert_eq!(display_name, "#123 (Camera)");
|
||||
|
||||
// Test with user component (should take precedence over Camera)
|
||||
entity.components.push("Player".to_string());
|
||||
entity.full_component_names.push("my_game::Player".to_string());
|
||||
|
||||
let display_name = generate_entity_display_name(&entity, None);
|
||||
assert_eq!(display_name, "#123 (Player)");
|
||||
|
||||
// Test with Name component (should take precedence over everything)
|
||||
entity.components.push("Name".to_string());
|
||||
entity.full_component_names.push("bevy_core::name::Name".to_string());
|
||||
|
||||
let display_name = generate_entity_display_name(&entity, None);
|
||||
assert_eq!(display_name, "#123 (Named Entity)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_component_precedence_over_bevy() {
|
||||
let entity = RemoteEntity {
|
||||
id: 456,
|
||||
components: vec!["Cube".to_string(), "Mesh3d".to_string(), "Transform".to_string()],
|
||||
full_component_names: vec![
|
||||
"my_game::Cube".to_string(),
|
||||
"bevy_mesh::mesh::Mesh3d".to_string(),
|
||||
"bevy_transform::components::Transform".to_string()
|
||||
],
|
||||
};
|
||||
|
||||
let display_name = generate_entity_display_name(&entity, None);
|
||||
// User component "Cube" should take precedence over "Mesh3d"
|
||||
assert_eq!(display_name, "#456 (Cube)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_to_entity_id() {
|
||||
let entity = RemoteEntity {
|
||||
id: 789,
|
||||
components: vec!["Transform".to_string(), "Visibility".to_string()],
|
||||
full_component_names: vec!["bevy_transform::components::Transform".to_string(), "bevy_render::view::visibility::Visibility".to_string()],
|
||||
};
|
||||
|
||||
let display_name = generate_entity_display_name(&entity, None);
|
||||
assert_eq!(display_name, "Entity 789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cube_example_from_server() {
|
||||
// Simulate the cube entity from server.rs example
|
||||
let entity = RemoteEntity {
|
||||
id: 42,
|
||||
components: vec![
|
||||
"Aabb".to_string(),
|
||||
"Cube".to_string(),
|
||||
"Mesh3d".to_string(),
|
||||
"MeshMaterial3d".to_string(),
|
||||
"Transform".to_string(),
|
||||
],
|
||||
full_component_names: vec![
|
||||
"bevy_camera::primitives::Aabb".to_string(),
|
||||
"server::Cube".to_string(),
|
||||
"bevy_mesh::mesh::Mesh3d".to_string(),
|
||||
"bevy_pbr::material::MeshMaterial3d".to_string(),
|
||||
"bevy_transform::components::Transform".to_string(),
|
||||
],
|
||||
};
|
||||
|
||||
let display_name = generate_entity_display_name(&entity, None);
|
||||
// User component "Cube" should take precedence over all Bevy components including Aabb
|
||||
assert_eq!(display_name, "#42 (Cube)");
|
||||
println!("Cube entity correctly named: {}", display_name);
|
||||
}
|
||||
}
|
@ -34,11 +34,13 @@
|
||||
pub mod types;
|
||||
pub mod client;
|
||||
pub mod connection;
|
||||
pub mod entity_naming;
|
||||
|
||||
use bevy::prelude::*;
|
||||
pub use types::*;
|
||||
pub use client::*;
|
||||
pub use connection::*;
|
||||
pub use entity_naming::*;
|
||||
|
||||
/// Plugin that handles remote connection functionality
|
||||
#[derive(Default)]
|
||||
|
@ -281,9 +281,27 @@ pub struct EntityListItem {
|
||||
|
||||
impl EntityListItem {
|
||||
pub fn from_remote_entity(remote_entity: &crate::remote::types::RemoteEntity) -> Self {
|
||||
// Use the smart naming system to generate a meaningful display name
|
||||
let display_name = crate::remote::entity_naming::generate_entity_display_name(remote_entity, None);
|
||||
|
||||
Self {
|
||||
entity_id: remote_entity.id,
|
||||
name: format!("Entity {}", remote_entity.id),
|
||||
name: display_name,
|
||||
components: remote_entity.components.clone(),
|
||||
children_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create EntityListItem with component data for better Name extraction
|
||||
pub fn from_remote_entity_with_data(
|
||||
remote_entity: &crate::remote::types::RemoteEntity,
|
||||
component_data: Option<&serde_json::Value>
|
||||
) -> Self {
|
||||
let display_name = crate::remote::entity_naming::generate_entity_display_name(remote_entity, component_data);
|
||||
|
||||
Self {
|
||||
entity_id: remote_entity.id,
|
||||
name: display_name,
|
||||
components: remote_entity.components.clone(),
|
||||
children_count: 0,
|
||||
}
|
||||
@ -292,7 +310,8 @@ impl EntityListItem {
|
||||
|
||||
impl ListDisplayable for EntityListItem {
|
||||
fn display_text(&self) -> String {
|
||||
format!("Entity {} ({})", self.entity_id, self.name)
|
||||
// The name already contains the smart formatting (e.g., "#123 (Camera)")
|
||||
self.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user