327 lines
11 KiB
Rust
327 lines
11 KiB
Rust
//! Detailed diagnostics (relocated from Status dock tab).
|
|
|
|
use bevy::prelude::*;
|
|
use bevy_egui::egui;
|
|
use shared::{
|
|
brush_math::{validate_brush, BrushDiagnosticSeverity},
|
|
ActorName, BrushDesc, LevelObject,
|
|
};
|
|
|
|
use crate::assets::EditorAssets;
|
|
use crate::diagnostics_bundle::export_diagnostic_bundle;
|
|
use crate::history::set_brush_with_history;
|
|
use crate::history::EditorHistory;
|
|
use crate::project_io::ProjectWorkspace;
|
|
use crate::scene_io::{SceneIo, SceneIoEventSeverity};
|
|
use crate::selection::SelectedEntity;
|
|
use crate::ui::UiState;
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct DiagnosticsPanel {
|
|
pub open: bool,
|
|
pub bundle_status: Option<String>,
|
|
pub project_validation: Option<scene::ProjectValidationReport>,
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
pub struct BrushDiagnosticsPanel {
|
|
pub open: bool,
|
|
}
|
|
|
|
pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
|
let (scene_label, dirty, status, scene_events) = {
|
|
let scene_io = world.resource::<SceneIo>();
|
|
(
|
|
scene_io.active_path_label(),
|
|
scene_io.dirty,
|
|
scene_io.status.clone(),
|
|
scene_io.events.iter().rev().cloned().collect::<Vec<_>>(),
|
|
)
|
|
};
|
|
let dirty = if dirty { " (dirty)" } else { "" };
|
|
ui.label(format!("Scene: {scene_label}{dirty}"));
|
|
ui.label(format!("Status: {status}"));
|
|
ui.label(format!(
|
|
"History: {}",
|
|
world.resource::<EditorHistory>().status
|
|
));
|
|
ui.label(format!(
|
|
"Assets: {}",
|
|
world.resource::<EditorAssets>().status
|
|
));
|
|
|
|
let workspace = world.resource::<ProjectWorkspace>();
|
|
ui.label(format!("Project root: {}", workspace.root));
|
|
ui.label(format!(
|
|
"Project settings: {}{}",
|
|
workspace.settings_path,
|
|
if workspace.settings_dirty {
|
|
" (dirty)"
|
|
} else {
|
|
""
|
|
}
|
|
));
|
|
if workspace.layout_dirty {
|
|
ui.small("Dock layout modified (unsaved)");
|
|
}
|
|
|
|
let mut level_query = world.query_filtered::<Entity, With<LevelObject>>();
|
|
let count = level_query.iter(world).count();
|
|
ui.label(format!("Level objects: {count}"));
|
|
|
|
ui.separator();
|
|
ui.horizontal(|ui| {
|
|
ui.strong("Project validation");
|
|
if ui.button("Validate Project").clicked() {
|
|
let root = world.resource::<ProjectWorkspace>().root.clone();
|
|
let report = scene::validate_project(std::path::Path::new(&root));
|
|
let summary = format!(
|
|
"Project validation: {} dependencies, {} findings, {} blocking errors",
|
|
report.dependencies.len(),
|
|
report.findings.len(),
|
|
report.blocking_error_count()
|
|
);
|
|
world.resource_mut::<SceneIo>().set_status(summary);
|
|
world.resource_mut::<DiagnosticsPanel>().project_validation = Some(report);
|
|
}
|
|
});
|
|
if let Some(report) = world
|
|
.resource::<DiagnosticsPanel>()
|
|
.project_validation
|
|
.as_ref()
|
|
.cloned()
|
|
{
|
|
let color = if report.is_release_ready() {
|
|
egui::Color32::from_rgb(125, 210, 145)
|
|
} else {
|
|
egui::Color32::from_rgb(255, 137, 129)
|
|
};
|
|
ui.colored_label(
|
|
color,
|
|
format!(
|
|
"{} dependencies, {} blocking errors",
|
|
report.dependencies.len(),
|
|
report.blocking_error_count()
|
|
),
|
|
);
|
|
for finding in report.findings.iter().take(12) {
|
|
let actor = finding
|
|
.owner_actor_id
|
|
.as_deref()
|
|
.map(|actor| format!(" / {actor}"))
|
|
.unwrap_or_default();
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.monospace(format!("{}{}", finding.source_path, actor));
|
|
ui.label(&finding.message);
|
|
if let Some(actor_id) = finding.owner_actor_id.as_deref() {
|
|
if ui.small_button("Select").clicked() {
|
|
let mut query = world.query::<(Entity, &shared::ActorId)>();
|
|
if let Some(entity) = query
|
|
.iter(world)
|
|
.find_map(|(entity, id)| (id.0 == actor_id).then_some(entity))
|
|
{
|
|
world.resource_mut::<SelectedEntity>().0 = Some(entity);
|
|
world
|
|
.resource_mut::<UiState>()
|
|
.selected_entities
|
|
.select_replace(entity);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if report.findings.len() > 12 {
|
|
ui.small(format!(
|
|
"{} additional findings",
|
|
report.findings.len() - 12
|
|
));
|
|
}
|
|
}
|
|
|
|
ui.separator();
|
|
ui.horizontal(|ui| {
|
|
ui.strong("Support bundle");
|
|
if ui.button("Export Diagnostic Bundle").clicked() {
|
|
let status = match export_diagnostic_bundle(world) {
|
|
Ok(path) => format!("Exported {}", path.display()),
|
|
Err(error) => format!("Export failed: {error}"),
|
|
};
|
|
world.resource_mut::<DiagnosticsPanel>().bundle_status = Some(status);
|
|
}
|
|
});
|
|
ui.small("Allowlisted metadata only; scene and asset contents, environment values, credentials, and modal state are excluded.");
|
|
if let Some(status) = world
|
|
.resource::<DiagnosticsPanel>()
|
|
.bundle_status
|
|
.as_deref()
|
|
{
|
|
ui.monospace(status);
|
|
}
|
|
|
|
ui.separator();
|
|
ui.horizontal(|ui| {
|
|
ui.strong("Scene I/O log");
|
|
if ui
|
|
.add_enabled(!scene_events.is_empty(), egui::Button::new("Clear"))
|
|
.clicked()
|
|
{
|
|
world.resource_mut::<SceneIo>().clear_events();
|
|
}
|
|
});
|
|
if scene_events.is_empty() {
|
|
ui.small("No scene I/O operations recorded this session.");
|
|
} else {
|
|
egui::ScrollArea::vertical()
|
|
.id_salt("scene_io_diagnostics_log")
|
|
.max_height(180.0)
|
|
.show(ui, |ui| {
|
|
for event in scene_events {
|
|
let color = match event.severity {
|
|
SceneIoEventSeverity::Info => ui.visuals().weak_text_color(),
|
|
SceneIoEventSeverity::Error => egui::Color32::from_rgb(244, 110, 105),
|
|
};
|
|
ui.horizontal_wrapped(|ui| {
|
|
ui.colored_label(color, format!("#{:02}", event.id));
|
|
ui.colored_label(color, event.message);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
ui.separator();
|
|
ui.small("Native Bevy scenes are saved to assets/levels/*.scn.ron");
|
|
ui.small("BRP is enabled on the editor for external tooling.");
|
|
}
|
|
|
|
pub fn brush_diagnostics_window(world: &mut World, ctx: &egui::Context, open: &mut bool) {
|
|
if !*open {
|
|
return;
|
|
}
|
|
egui::Window::new("Brush Diagnostics")
|
|
.open(open)
|
|
.default_width(520.0)
|
|
.show(ctx, |ui| {
|
|
brush_diagnostics_ui(world, ui);
|
|
});
|
|
}
|
|
|
|
fn brush_diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
|
let rows = brush_diagnostic_rows(world);
|
|
let invalid_count = rows.iter().filter(|row| row.error_count > 0).count();
|
|
ui.label(format!(
|
|
"{} brush actor(s), {} invalid",
|
|
rows.len(),
|
|
invalid_count
|
|
));
|
|
ui.separator();
|
|
if rows.is_empty() {
|
|
ui.small("No brush actors in the current scene.");
|
|
return;
|
|
}
|
|
|
|
for row in rows {
|
|
ui.horizontal_wrapped(|ui| {
|
|
let color = if row.error_count > 0 {
|
|
egui::Color32::from_rgb(255, 137, 129)
|
|
} else if row.warning_count > 0 {
|
|
egui::Color32::from_rgb(255, 190, 110)
|
|
} else {
|
|
egui::Color32::from_rgb(125, 210, 145)
|
|
};
|
|
ui.colored_label(color, row.status_label());
|
|
ui.label(row.name.as_str());
|
|
if ui.button("Select").clicked() {
|
|
world.resource_mut::<SelectedEntity>().0 = Some(row.entity);
|
|
world
|
|
.resource_mut::<UiState>()
|
|
.selected_entities
|
|
.select_replace(row.entity);
|
|
}
|
|
if ui
|
|
.add_enabled(row.error_count > 0, egui::Button::new("Reset Cube"))
|
|
.clicked()
|
|
{
|
|
set_brush_with_history(world, row.entity, BrushDesc::default());
|
|
}
|
|
});
|
|
if let Some(message) = row.first_message.as_ref() {
|
|
ui.small(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct BrushDiagnosticRow {
|
|
entity: Entity,
|
|
name: String,
|
|
error_count: usize,
|
|
warning_count: usize,
|
|
first_message: Option<String>,
|
|
}
|
|
|
|
impl BrushDiagnosticRow {
|
|
fn status_label(&self) -> String {
|
|
if self.error_count > 0 {
|
|
format!("{} error(s)", self.error_count)
|
|
} else if self.warning_count > 0 {
|
|
format!("{} warning(s)", self.warning_count)
|
|
} else {
|
|
"valid".into()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn brush_diagnostic_rows(world: &mut World) -> Vec<BrushDiagnosticRow> {
|
|
let mut query =
|
|
world.query_filtered::<(Entity, Option<&ActorName>, Option<&Name>, &BrushDesc), With<LevelObject>>();
|
|
let mut rows: Vec<_> = query
|
|
.iter(world)
|
|
.map(|(entity, actor_name, name, brush)| {
|
|
let report = validate_brush(brush);
|
|
let error_count = report
|
|
.diagnostics
|
|
.iter()
|
|
.filter(|diagnostic| diagnostic.severity == BrushDiagnosticSeverity::Error)
|
|
.count();
|
|
let warning_count = report
|
|
.diagnostics
|
|
.iter()
|
|
.filter(|diagnostic| diagnostic.severity == BrushDiagnosticSeverity::Warning)
|
|
.count();
|
|
let first_message = report
|
|
.diagnostics
|
|
.first()
|
|
.map(|diagnostic| diagnostic.message.clone());
|
|
BrushDiagnosticRow {
|
|
entity,
|
|
name: actor_name
|
|
.map(|name| name.0.clone())
|
|
.or_else(|| name.map(|name| name.to_string()))
|
|
.unwrap_or_else(|| format!("{entity:?}")),
|
|
error_count,
|
|
warning_count,
|
|
first_message,
|
|
}
|
|
})
|
|
.collect();
|
|
rows.sort_by(|a, b| {
|
|
b.error_count
|
|
.cmp(&a.error_count)
|
|
.then(b.warning_count.cmp(&a.warning_count))
|
|
.then(a.name.cmp(&b.name))
|
|
});
|
|
rows
|
|
}
|
|
|
|
pub fn diagnostics_window(world: &mut World, ctx: &egui::Context, open: &mut bool) {
|
|
if !*open {
|
|
return;
|
|
}
|
|
egui::Window::new("Diagnostics")
|
|
.open(open)
|
|
.default_width(420.0)
|
|
.show(ctx, |ui| {
|
|
diagnostics_ui(world, ui);
|
|
});
|
|
}
|