Block saving invalid brushes
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run

This commit is contained in:
Rbanh 2026-06-06 06:22:35 -04:00
parent 015ae6da63
commit 11b0df2b5f
3 changed files with 36 additions and 5 deletions

View File

@ -404,6 +404,9 @@ fn format_actor_validation(err: ActorValidationError) -> String {
ActorValidationError::BrushHasModelRef => { ActorValidationError::BrushHasModelRef => {
"Save failed: Brush actor cannot have ModelRef".into() "Save failed: Brush actor cannot have ModelRef".into()
} }
ActorValidationError::InvalidBrushGeometry(message) => {
format!("Save failed: invalid brush geometry: {message}")
}
ActorValidationError::StaticMeshMissingPrimitive => { ActorValidationError::StaticMeshMissingPrimitive => {
"Save failed: StaticMesh actor requires Primitive or StaticMeshRenderer with a mesh slot" "Save failed: StaticMesh actor requires Primitive or StaticMeshRenderer with a mesh slot"
.into() .into()

View File

@ -4,9 +4,9 @@ use bevy::ecs::world::EntityRef;
use bevy::prelude::*; use bevy::prelude::*;
use crate::{ use crate::{
ActorKind, BrushDesc, LevelObject, LightDesc, ModelRef, ObjectiveMarker, PlayerSpawn, brush_math::validate_brush, ActorKind, BrushDesc, LevelObject, LightDesc, ModelRef,
PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive, StaticMeshRenderer, TeamSpawn, ObjectiveMarker, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive,
TriggerVolume, WeaponSpawn, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn,
}; };
/// One-shot deterministic kind from authoring components (scene migration only). /// One-shot deterministic kind from authoring components (scene migration only).
@ -59,6 +59,7 @@ pub enum ActorValidationError {
BrushHasStaticMeshRenderer, BrushHasStaticMeshRenderer,
BrushHasLight, BrushHasLight,
BrushHasModelRef, BrushHasModelRef,
InvalidBrushGeometry(String),
StaticMeshMissingPrimitive, StaticMeshMissingPrimitive,
StaticMeshHasLight, StaticMeshHasLight,
StaticMeshHasModelRef, StaticMeshHasModelRef,
@ -99,9 +100,9 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
match kind { match kind {
ActorKind::Brush => { ActorKind::Brush => {
if entity.get::<BrushDesc>().is_none() { let Some(brush) = entity.get::<BrushDesc>() else {
return Err(ActorValidationError::BrushMissingDesc); return Err(ActorValidationError::BrushMissingDesc);
} };
if entity.get::<Primitive>().is_some() { if entity.get::<Primitive>().is_some() {
return Err(ActorValidationError::BrushHasPrimitive); return Err(ActorValidationError::BrushHasPrimitive);
} }
@ -114,6 +115,18 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError>
if entity.get::<ModelRef>().is_some() { if entity.get::<ModelRef>().is_some() {
return Err(ActorValidationError::BrushHasModelRef); return Err(ActorValidationError::BrushHasModelRef);
} }
let report = validate_brush(brush);
if !report.is_valid() {
let message = report
.diagnostics
.into_iter()
.find(|diagnostic| {
diagnostic.severity == crate::brush_math::BrushDiagnosticSeverity::Error
})
.map(|diagnostic| diagnostic.message)
.unwrap_or_else(|| "Brush geometry is invalid.".to_string());
return Err(ActorValidationError::InvalidBrushGeometry(message));
}
} }
ActorKind::StaticMesh => { ActorKind::StaticMesh => {
let has_static_mesh_renderer = entity let has_static_mesh_renderer = entity
@ -262,6 +275,20 @@ mod tests {
); );
} }
#[test]
fn validate_brush_rejects_invalid_geometry() {
let mut world = World::new();
let mut brush = BrushDesc::default();
brush.faces[0].vertices.clear();
let e = level_entity(&mut world, (ActorKind::Brush, brush));
assert_eq!(
validate_actor(world.entity(e)),
Err(ActorValidationError::InvalidBrushGeometry(
"Face has fewer than three vertices.".into()
))
);
}
#[test] #[test]
fn validate_post_process_volume_ok() { fn validate_post_process_volume_ok() {
let mut world = World::new(); let mut world = World::new();

View File

@ -10,6 +10,7 @@ Brushes are persisted blockout geometry stored as `ActorKind::Brush + BrushDesc`
- Brush actors validate separately from primitives and imported/static mesh actors. - Brush actors validate separately from primitives and imported/static mesh actors.
- 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.
- 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