Add brush diagnostics window
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run

This commit is contained in:
Rbanh 2026-06-06 06:30:18 -04:00
parent 8abe47047d
commit 09f707b139
5 changed files with 149 additions and 5 deletions

View File

@ -2,18 +2,29 @@
use bevy::prelude::*;
use bevy_egui::egui;
use shared::LevelObject;
use shared::{
brush_math::{validate_brush, BrushDiagnosticSeverity},
ActorName, BrushDesc, LevelObject,
};
use crate::assets::EditorAssets;
use crate::history::set_brush_with_history;
use crate::history::EditorHistory;
use crate::project_io::ProjectWorkspace;
use crate::scene_io::SceneIo;
use crate::selection::SelectedEntity;
use crate::ui::UiState;
#[derive(Resource, Default)]
pub struct DiagnosticsPanel {
pub open: bool,
}
#[derive(Resource, Default)]
pub struct BrushDiagnosticsPanel {
pub open: bool,
}
pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
let scene_io = world.resource::<SceneIo>();
let dirty = if scene_io.dirty { " (dirty)" } else { "" };
@ -52,6 +63,126 @@ pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
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;

View File

@ -11,7 +11,7 @@ use crate::settings_ui::{open_project_settings_panel, ProjectSettingsPanel};
use crate::state::{EditorMode, PlayPaused};
use crate::workspace::{request_new_project, request_open_project};
use super::diagnostics::DiagnosticsPanel;
use super::diagnostics::{BrushDiagnosticsPanel, DiagnosticsPanel};
use super::dock_tabs::{open_and_focus_tab, tab_is_open, tab_label, PanelNodes, PANEL_TABS};
use super::helpers::{
delete_selection, duplicate_selection, toggle_play_mode, toggle_play_paused, toggle_possession,
@ -159,6 +159,13 @@ pub fn top_menu_bar(
if ui.checkbox(&mut diagnostics.open, "Diagnostics").clicked() {
ui.close();
}
let mut brush_diagnostics = world.resource_mut::<BrushDiagnosticsPanel>();
if ui
.checkbox(&mut brush_diagnostics.open, "Brush Diagnostics")
.clicked()
{
ui.close();
}
let mut rendering =
world.resource_mut::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>();
if ui.checkbox(&mut rendering.open, "Rendering").clicked() {

View File

@ -34,7 +34,7 @@ use crate::project_io::UserPreferences;
use crate::selection::SelectedEntity;
use crate::state::EditorMode;
pub use diagnostics::DiagnosticsPanel;
pub use diagnostics::{BrushDiagnosticsPanel, DiagnosticsPanel};
pub use layout::LayoutSaveTimer;
pub use viewport_chrome::ViewportUiState;
@ -184,6 +184,10 @@ impl UiState {
diagnostics_window(world, ctx, &mut diagnostics_open);
world.resource_mut::<DiagnosticsPanel>().open = diagnostics_open;
let mut brush_diagnostics_open = world.resource::<BrushDiagnosticsPanel>().open;
diagnostics::brush_diagnostics_window(world, ctx, &mut brush_diagnostics_open);
world.resource_mut::<BrushDiagnosticsPanel>().open = brush_diagnostics_open;
let mut rendering_open = world
.resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
.open;
@ -246,6 +250,7 @@ impl Plugin for EditorUiPlugin {
app.insert_resource(UiState::default_layout())
.add_plugins(fonts::EditorFontsPlugin)
.init_resource::<DiagnosticsPanel>()
.init_resource::<BrushDiagnosticsPanel>()
.init_resource::<component_registry::EditorComponentRegistry>()
.init_resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
.init_resource::<asset_browser::AssetBrowserUiState>()

View File

@ -43,7 +43,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
- **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera.
- **Asset browser** mirrors the on-disk `assets/` directory with project tree, breadcrumb, search/filter/sort, grid/list views, texture/model/material thumbnails, a details pane, and context-aware row/menu actions; narrow docks prioritize content, keep the root panel fixed, switch list view to a compact single-column layout, and hide tree/details panes when they would crowd the content area. The footer stays pinned while only the project tree, asset content, and details panels scroll. **Built-ins** holds spawnable primitives and lights. **Materials** folder scans `assets/materials/*.ron`, renders material thumbnails on a sphere using `MaterialDesc`, and exposes shader-schema-driven parameters/textures in the details editor; **Shaders** holds shader schema RON files. glTF/GLB/FBX rows can expand into a shelf of normalized embedded mesh, material, and texture subassets with independent generated thumbnails. Mesh subassets can be selected, dragged into the viewport, or placed from details/context menus; material subassets render source-material spheres; texture subassets can be applied to the selected actor. Model import settings are staged with **Apply** / **Revert**, asset context menus can regenerate thumbnails, material asset details edit shared `MaterialAsset` fields, and file asset deletion moves sources/generated artifacts into `assets/.trash/`.
- **Static mesh renderer** — model drag/drop defaults to `ActorKind::StaticMesh + StaticMeshRenderer` using normalized artifacts under `assets/meshes/generated/`. Renderer slots store imported `EditorAssetRef` mesh/material references, not source FBX/glTF paths. `SceneInstance` placement keeps the legacy `ImportedModel + ModelRef` path for full scene playback.
- **Brush authoring**`ActorKind::Brush + BrushDesc` stores persisted convex blockout faces, validates authored geometry in the inspector, and hydrates active valid brushes into generated mesh children. See [brushes.md](brushes.md).
- **Brush authoring**`ActorKind::Brush + BrushDesc` stores persisted convex blockout faces, validates authored geometry in the inspector and Window → Brush Diagnostics, and hydrates active valid brushes into generated mesh children. See [brushes.md](brushes.md).
- **Draw Brush**`B`, toolbar pencil, or command `brush.draw` enters a floor-polygon draw mode. LMB places snapped points, Backspace removes the last point, Enter creates an additive prism brush through history, and Esc/right-click cancels.
- **Brush edit modes** — with a brush selected, `1`/`2`/`3`/`4` enter vertex, edge, face, and clip element modes. Element modes show brush handles in the viewport, own LMB picking, support Shift multi-select, show a mode badge, and Esc returns to object mode. Vertex/edge/face selections can be dragged on the viewport floor plane and commit undoable `SetBrush` edits; clip mode is a preview/selection mode until CSG split support lands.
- **Collider split** — imported mesh collision lives in a separate `ColliderDesc::StaticMesh` plus optional `RigidBodyDesc`; renderer slots own only render visibility, shadows, mesh, and material references.

View File

@ -11,6 +11,7 @@ Brushes are persisted blockout geometry stored as `ActorKind::Brush + BrushDesc`
- The Actor Inspector Add Component shelf can add a **Brush** component. The Brush card exposes kind, face count, shadow flags, and cube reset.
- The Brush card runs shared validation and reports invalid faces inline. Fatal geometry errors prevent hydration; warnings call out authoring issues that can still render. **Reset Cube Brush** is the MVP repair path.
- Scene save runs the same fatal brush validation and blocks writing unrecoverable invalid brush geometry.
- **Window → Brush Diagnostics** lists all brush actors, surfaces validation counts/messages, can select the affected brush, and provides an undoable **Reset Cube** repair for invalid brushes.
- Draw Brush mode (`B`, toolbar pencil, or command `brush.draw`) creates additive prism brushes from snapped floor points. LMB places points, Backspace removes the last point, Enter commits through undo history, and Esc/right-click cancels without scene mutation.
## Element Modes
@ -34,7 +35,7 @@ These operations validate selected brushes before applying results and validate
## Current Limits
- Only convex authored faces are supported by the mesh builder.
- Brush diagnostics currently focus on face validity, plane normals, finite vertices, duplicate vertices, degenerate area, and UV scale warnings; a dedicated multi-brush diagnostics window is future work.
- Brush diagnostics currently focus on face validity, plane normals, finite vertices, duplicate vertices, degenerate area, and UV scale warnings.
- Subtractive brush markers are stored but not automatically evaluated.
- Clip split application and arbitrary-face CSG remain future roadmap work.
- Per-face material and texture refs persist and are editable through Asset Browser-aware controls, but generated brush meshes still hydrate through a single material batch.