From 3f85d25cc7a2d8bb2268323e97ae98af3930d45a Mon Sep 17 00:00:00 2001 From: Rbanh Date: Sat, 6 Jun 2026 14:22:36 -0400 Subject: [PATCH] Group brush CSG history edits --- crates/editor/src/history/commands.rs | 9 +++ crates/editor/src/history/mod.rs | 81 +++++++++++++++++++++++++ crates/editor/src/viewport/brush_csg.rs | 36 +++++++---- 3 files changed, 115 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/history/commands.rs b/crates/editor/src/history/commands.rs index c927138..1ce3c42 100644 --- a/crates/editor/src/history/commands.rs +++ b/crates/editor/src/history/commands.rs @@ -119,6 +119,14 @@ pub enum EditorCommand { old_brush: Option, new_brush: BrushDesc, }, + ApplyBrushCsg { + primary: Entity, + old_transform: Transform, + new_transform: Transform, + old_brush: Option, + new_brush: BrushDesc, + deleted: Vec<(EditorEntitySnapshot, Option)>, + }, SetStaticMeshRenderer { entity: Entity, old: Option, @@ -185,6 +193,7 @@ impl EditorCommand { EditorCommand::SetPrimitive { .. } => "Set Primitive", EditorCommand::SetBrush { .. } => "Set Brush", EditorCommand::SetBrushTransform { .. } => "Clip Brush", + EditorCommand::ApplyBrushCsg { .. } => "Brush CSG", EditorCommand::SetStaticMeshRenderer { .. } => "Set Static Mesh Renderer", EditorCommand::SetPostProcessVolume { .. } => "Set Post Process Volume", EditorCommand::SetTransformGroup { .. } => "Move Selection", diff --git a/crates/editor/src/history/mod.rs b/crates/editor/src/history/mod.rs index 10820d0..7a49add 100644 --- a/crates/editor/src/history/mod.rs +++ b/crates/editor/src/history/mod.rs @@ -529,6 +529,52 @@ pub fn set_brush_transform_with_history( ); } +pub fn apply_brush_csg_with_history( + world: &mut World, + primary: Entity, + new_transform: Transform, + new_brush: BrushDesc, + delete_entities: &[Entity], +) { + if !is_level_object(world, primary) { + return; + } + let old_transform = world.get::(primary).copied().unwrap_or_default(); + let old_brush = world.get::(primary).cloned(); + let deleted: Vec<(EditorEntitySnapshot, Option)> = delete_entities + .iter() + .copied() + .filter_map(|entity| { + snapshot_entity(world, entity).map(|snapshot| (snapshot, Some(entity))) + }) + .collect(); + if old_transform == new_transform + && old_brush.as_ref() == Some(&new_brush) + && deleted.is_empty() + { + return; + } + if let Ok(mut entity_mut) = world.get_entity_mut(primary) { + entity_mut.insert(new_transform); + entity_mut.insert(new_brush.clone()); + } + for (_, entity) in &deleted { + despawn_entity(world, *entity); + } + push_history( + world, + EditorCommand::ApplyBrushCsg { + primary, + old_transform, + new_transform, + old_brush, + new_brush, + deleted, + }, + ); + select_one(world, primary); +} + pub fn set_static_mesh_renderer_with_history( world: &mut World, entity: Entity, @@ -875,6 +921,25 @@ fn undo_command(world: &mut World, command: &mut EditorCommand) { } apply_brush(world, *entity, old_brush); } + EditorCommand::ApplyBrushCsg { + primary, + old_transform, + old_brush, + deleted, + .. + } => { + if let Some(mut transform) = world.get_mut::(*primary) { + *transform = *old_transform; + } + apply_brush(world, *primary, old_brush); + let mut restored = vec![*primary]; + for (snapshot, entity) in deleted.iter_mut() { + let spawned = spawn_snapshot(world, snapshot); + *entity = Some(spawned); + restored.push(spawned); + } + select_many(world, &restored); + } EditorCommand::SetStaticMeshRenderer { entity, old, .. } => { apply_static_mesh_renderer(world, *entity, old); } @@ -1014,6 +1079,22 @@ fn redo_command(world: &mut World, command: &mut EditorCommand) { entity_mut.insert(new_brush.clone()); } } + EditorCommand::ApplyBrushCsg { + primary, + new_transform, + new_brush, + deleted, + .. + } => { + if let Ok(mut entity_mut) = world.get_entity_mut(*primary) { + entity_mut.insert(*new_transform); + entity_mut.insert(new_brush.clone()); + } + for (_, entity) in deleted.iter_mut() { + despawn_entity(world, entity.take()); + } + select_one(world, *primary); + } EditorCommand::SetStaticMeshRenderer { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); diff --git a/crates/editor/src/viewport/brush_csg.rs b/crates/editor/src/viewport/brush_csg.rs index 3cbeefa..715f090 100644 --- a/crates/editor/src/viewport/brush_csg.rs +++ b/crates/editor/src/viewport/brush_csg.rs @@ -7,9 +7,7 @@ use shared::{ BrushDesc, LevelObject, }; -use crate::history::{ - delete_entities_with_history, set_brush_with_history, set_transform_with_history, -}; +use crate::history::apply_brush_csg_with_history; use crate::scene_io::SceneIo; use crate::ui::UiState; @@ -241,17 +239,18 @@ fn commit_csg_preview(world: &mut World, pending: PendingBrushCsg) { return; } let new_brush = BrushDesc::cuboid(pending.result.size()); - apply_bounds_result(world, primary, pending.result, new_brush); - if matches!(pending.op, BrushCsgOp::Merge | BrushCsgOp::Intersect) { - let to_delete: Vec<_> = pending + let to_delete: Vec<_> = if matches!(pending.op, BrushCsgOp::Merge | BrushCsgOp::Intersect) { + pending .selected .iter() .skip(1) .copied() .filter(|entity| world.get::(*entity).is_some()) - .collect(); - delete_entities_with_history(world, &to_delete); - } + .collect() + } else { + Vec::new() + }; + apply_bounds_result(world, primary, pending.result, new_brush, &to_delete); set_status( world, match pending.op { @@ -318,6 +317,7 @@ fn apply_bounds_result( entity: Entity, bounds: BrushBounds, new_brush: BrushDesc, + delete_entities: &[Entity], ) { let center = bounds.center(); let old_transform = world.get::(entity).copied().unwrap_or_default(); @@ -325,8 +325,7 @@ fn apply_bounds_result( new_transform.translation = center; new_transform.rotation = Quat::IDENTITY; new_transform.scale = Vec3::ONE; - set_transform_with_history(world, entity, old_transform, new_transform); - set_brush_with_history(world, entity, new_brush); + apply_brush_csg_with_history(world, entity, new_transform, new_brush, delete_entities); } fn brush_world_bounds(world: &World, entity: Entity) -> Option { @@ -398,6 +397,7 @@ fn set_status(world: &mut World, status: impl Into) { #[cfg(test)] mod tests { use super::*; + use crate::history::EditorCommand; fn bounds(min: Vec3, max: Vec3) -> BrushBounds { BrushBounds { min, max } @@ -418,4 +418,18 @@ mod tests { assert_eq!(result.min, Vec3::new(1.0, 0.0, 0.0)); assert_eq!(result.max, source.max); } + + #[test] + fn csg_history_command_is_grouped() { + let command = EditorCommand::ApplyBrushCsg { + primary: Entity::PLACEHOLDER, + old_transform: Transform::default(), + new_transform: Transform::from_translation(Vec3::X), + old_brush: Some(BrushDesc::default()), + new_brush: BrushDesc::default(), + deleted: Vec::new(), + }; + + assert_eq!(command.label(), "Brush CSG"); + } }