Group brush CSG history edits

This commit is contained in:
Rbanh 2026-06-06 14:22:36 -04:00
parent 1b9fbebda1
commit 3f85d25cc7
3 changed files with 115 additions and 11 deletions

View File

@ -119,6 +119,14 @@ pub enum EditorCommand {
old_brush: Option<BrushDesc>,
new_brush: BrushDesc,
},
ApplyBrushCsg {
primary: Entity,
old_transform: Transform,
new_transform: Transform,
old_brush: Option<BrushDesc>,
new_brush: BrushDesc,
deleted: Vec<(EditorEntitySnapshot, Option<Entity>)>,
},
SetStaticMeshRenderer {
entity: Entity,
old: Option<StaticMeshRenderer>,
@ -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",

View File

@ -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::<Transform>(primary).copied().unwrap_or_default();
let old_brush = world.get::<BrushDesc>(primary).cloned();
let deleted: Vec<(EditorEntitySnapshot, Option<Entity>)> = 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::<Transform>(*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());

View File

@ -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::<BrushDesc>(*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::<Transform>(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<BrushBounds> {
@ -398,6 +397,7 @@ fn set_status(world: &mut World, status: impl Into<String>) {
#[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");
}
}