From 11b0df2b5f567801aef3b63c5b03c0060f0fd745 Mon Sep 17 00:00:00 2001 From: Rbanh Date: Sat, 6 Jun 2026 06:22:35 -0400 Subject: [PATCH] Block saving invalid brushes --- crates/editor/src/scene/scene_io.rs | 3 +++ crates/shared/src/actor.rs | 37 +++++++++++++++++++++++++---- docs/editor/brushes.md | 1 + 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/scene/scene_io.rs b/crates/editor/src/scene/scene_io.rs index 5d2637c..d3c8c76 100644 --- a/crates/editor/src/scene/scene_io.rs +++ b/crates/editor/src/scene/scene_io.rs @@ -404,6 +404,9 @@ fn format_actor_validation(err: ActorValidationError) -> String { ActorValidationError::BrushHasModelRef => { "Save failed: Brush actor cannot have ModelRef".into() } + ActorValidationError::InvalidBrushGeometry(message) => { + format!("Save failed: invalid brush geometry: {message}") + } ActorValidationError::StaticMeshMissingPrimitive => { "Save failed: StaticMesh actor requires Primitive or StaticMeshRenderer with a mesh slot" .into() diff --git a/crates/shared/src/actor.rs b/crates/shared/src/actor.rs index bd59ba6..f50f5fe 100644 --- a/crates/shared/src/actor.rs +++ b/crates/shared/src/actor.rs @@ -4,9 +4,9 @@ use bevy::ecs::world::EntityRef; use bevy::prelude::*; use crate::{ - ActorKind, BrushDesc, LevelObject, LightDesc, ModelRef, ObjectiveMarker, PlayerSpawn, - PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive, StaticMeshRenderer, TeamSpawn, - TriggerVolume, WeaponSpawn, + brush_math::validate_brush, ActorKind, BrushDesc, LevelObject, LightDesc, ModelRef, + ObjectiveMarker, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive, + StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn, }; /// One-shot deterministic kind from authoring components (scene migration only). @@ -59,6 +59,7 @@ pub enum ActorValidationError { BrushHasStaticMeshRenderer, BrushHasLight, BrushHasModelRef, + InvalidBrushGeometry(String), StaticMeshMissingPrimitive, StaticMeshHasLight, StaticMeshHasModelRef, @@ -99,9 +100,9 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError> match kind { ActorKind::Brush => { - if entity.get::().is_none() { + let Some(brush) = entity.get::() else { return Err(ActorValidationError::BrushMissingDesc); - } + }; if entity.get::().is_some() { return Err(ActorValidationError::BrushHasPrimitive); } @@ -114,6 +115,18 @@ pub fn validate_actor(entity: EntityRef<'_>) -> Result<(), ActorValidationError> if entity.get::().is_some() { 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 => { 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] fn validate_post_process_volume_ok() { let mut world = World::new(); diff --git a/docs/editor/brushes.md b/docs/editor/brushes.md index 39bb359..7614b97 100644 --- a/docs/editor/brushes.md +++ b/docs/editor/brushes.md @@ -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. - 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. - 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