feat(editor): implement smart entity naming system for improved display names

This commit is contained in:
jbuehler23 2025-07-18 16:59:39 +01:00
parent 9e889396db
commit 9993b202e0
4 changed files with 339 additions and 100 deletions

View File

@ -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>,

View 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);
}
}

View File

@ -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)]

View File

@ -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()
}
}