Add brush diagnostics window
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
This commit is contained in:
parent
8abe47047d
commit
09f707b139
@ -2,18 +2,29 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_egui::egui;
|
use bevy_egui::egui;
|
||||||
use shared::LevelObject;
|
use shared::{
|
||||||
|
brush_math::{validate_brush, BrushDiagnosticSeverity},
|
||||||
|
ActorName, BrushDesc, LevelObject,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::assets::EditorAssets;
|
use crate::assets::EditorAssets;
|
||||||
|
use crate::history::set_brush_with_history;
|
||||||
use crate::history::EditorHistory;
|
use crate::history::EditorHistory;
|
||||||
use crate::project_io::ProjectWorkspace;
|
use crate::project_io::ProjectWorkspace;
|
||||||
use crate::scene_io::SceneIo;
|
use crate::scene_io::SceneIo;
|
||||||
|
use crate::selection::SelectedEntity;
|
||||||
|
use crate::ui::UiState;
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct DiagnosticsPanel {
|
pub struct DiagnosticsPanel {
|
||||||
pub open: bool,
|
pub open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct BrushDiagnosticsPanel {
|
||||||
|
pub open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
pub fn diagnostics_ui(world: &mut World, ui: &mut egui::Ui) {
|
||||||
let scene_io = world.resource::<SceneIo>();
|
let scene_io = world.resource::<SceneIo>();
|
||||||
let dirty = if scene_io.dirty { " (dirty)" } else { "" };
|
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.");
|
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) {
|
pub fn diagnostics_window(world: &mut World, ctx: &egui::Context, open: &mut bool) {
|
||||||
if !*open {
|
if !*open {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ use crate::settings_ui::{open_project_settings_panel, ProjectSettingsPanel};
|
|||||||
use crate::state::{EditorMode, PlayPaused};
|
use crate::state::{EditorMode, PlayPaused};
|
||||||
use crate::workspace::{request_new_project, request_open_project};
|
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::dock_tabs::{open_and_focus_tab, tab_is_open, tab_label, PanelNodes, PANEL_TABS};
|
||||||
use super::helpers::{
|
use super::helpers::{
|
||||||
delete_selection, duplicate_selection, toggle_play_mode, toggle_play_paused, toggle_possession,
|
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() {
|
if ui.checkbox(&mut diagnostics.open, "Diagnostics").clicked() {
|
||||||
ui.close();
|
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 =
|
let mut rendering =
|
||||||
world.resource_mut::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>();
|
world.resource_mut::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>();
|
||||||
if ui.checkbox(&mut rendering.open, "Rendering").clicked() {
|
if ui.checkbox(&mut rendering.open, "Rendering").clicked() {
|
||||||
|
|||||||
@ -34,7 +34,7 @@ use crate::project_io::UserPreferences;
|
|||||||
use crate::selection::SelectedEntity;
|
use crate::selection::SelectedEntity;
|
||||||
use crate::state::EditorMode;
|
use crate::state::EditorMode;
|
||||||
|
|
||||||
pub use diagnostics::DiagnosticsPanel;
|
pub use diagnostics::{BrushDiagnosticsPanel, DiagnosticsPanel};
|
||||||
pub use layout::LayoutSaveTimer;
|
pub use layout::LayoutSaveTimer;
|
||||||
pub use viewport_chrome::ViewportUiState;
|
pub use viewport_chrome::ViewportUiState;
|
||||||
|
|
||||||
@ -184,6 +184,10 @@ impl UiState {
|
|||||||
diagnostics_window(world, ctx, &mut diagnostics_open);
|
diagnostics_window(world, ctx, &mut diagnostics_open);
|
||||||
world.resource_mut::<DiagnosticsPanel>().open = 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
|
let mut rendering_open = world
|
||||||
.resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
|
.resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
|
||||||
.open;
|
.open;
|
||||||
@ -246,6 +250,7 @@ impl Plugin for EditorUiPlugin {
|
|||||||
app.insert_resource(UiState::default_layout())
|
app.insert_resource(UiState::default_layout())
|
||||||
.add_plugins(fonts::EditorFontsPlugin)
|
.add_plugins(fonts::EditorFontsPlugin)
|
||||||
.init_resource::<DiagnosticsPanel>()
|
.init_resource::<DiagnosticsPanel>()
|
||||||
|
.init_resource::<BrushDiagnosticsPanel>()
|
||||||
.init_resource::<component_registry::EditorComponentRegistry>()
|
.init_resource::<component_registry::EditorComponentRegistry>()
|
||||||
.init_resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
|
.init_resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
|
||||||
.init_resource::<asset_browser::AssetBrowserUiState>()
|
.init_resource::<asset_browser::AssetBrowserUiState>()
|
||||||
|
|||||||
@ -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.
|
- **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/`.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@ -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 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.
|
- 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.
|
- 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.
|
- 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
|
## Element Modes
|
||||||
@ -34,7 +35,7 @@ These operations validate selected brushes before applying results and validate
|
|||||||
## Current Limits
|
## Current Limits
|
||||||
|
|
||||||
- Only convex authored faces are supported by the mesh builder.
|
- 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.
|
- Subtractive brush markers are stored but not automatically evaluated.
|
||||||
- Clip split application and arbitrary-face CSG remain future roadmap work.
|
- 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.
|
- 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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user