Preview brush CSG operations
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run

This commit is contained in:
Rbanh 2026-06-06 06:43:25 -04:00
parent b2305932d9
commit 4b6617f7f2
4 changed files with 138 additions and 7 deletions

View File

@ -29,6 +29,7 @@ pub use scene::scene_io;
pub use scene::scene_schema; pub use scene::scene_schema;
pub use scene::scene_view; pub use scene::scene_view;
pub use viewport::actor_icons; pub use viewport::actor_icons;
pub use viewport::brush_csg;
pub use viewport::brush_edit; pub use viewport::brush_edit;
pub use viewport::brush_tool; pub use viewport::brush_tool;
pub use viewport::camera; pub use viewport::camera;
@ -49,6 +50,7 @@ use transform_gizmo_bevy::prelude::TransformGizmoPlugin;
use actor_icons::ActorIconsPlugin; use actor_icons::ActorIconsPlugin;
use asset_db::AssetDbPlugin; use asset_db::AssetDbPlugin;
use brush_csg::BrushCsgPlugin;
use brush_edit::BrushEditPlugin; use brush_edit::BrushEditPlugin;
use brush_tool::BrushToolPlugin; use brush_tool::BrushToolPlugin;
use camera::EditorCameraPlugin; use camera::EditorCameraPlugin;
@ -93,6 +95,7 @@ impl PluginGroup for EditorPluginGroup {
.add(SceneViewPlugin) .add(SceneViewPlugin)
.add(PlaySessionPlugin) .add(PlaySessionPlugin)
.add(ViewportPlugin) .add(ViewportPlugin)
.add(BrushCsgPlugin)
.add(BrushEditPlugin) .add(BrushEditPlugin)
.add(BrushToolPlugin) .add(BrushToolPlugin)
.add(EditorCameraPlugin) .add(EditorCameraPlugin)

View File

@ -13,6 +13,18 @@ use crate::history::{
use crate::scene_io::SceneIo; use crate::scene_io::SceneIo;
use crate::ui::UiState; use crate::ui::UiState;
pub struct BrushCsgPlugin;
impl Plugin for BrushCsgPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<BrushCsgPreview>().add_systems(
Update,
(brush_csg_preview_input, draw_brush_csg_preview)
.run_if(crate::state::scene_tools_active),
);
}
}
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
struct BrushBounds { struct BrushBounds {
min: Vec3, min: Vec3,
@ -69,7 +81,19 @@ pub fn subtract_selected_brushes(world: &mut World) {
run_bounds_csg(world, BrushCsgOp::Subtract); run_bounds_csg(world, BrushCsgOp::Subtract);
} }
#[derive(Debug, Clone, Copy)] #[derive(Resource, Default, Debug, Clone)]
struct BrushCsgPreview {
pending: Option<PendingBrushCsg>,
}
#[derive(Debug, Clone)]
struct PendingBrushCsg {
op: BrushCsgOp,
selected: Vec<Entity>,
result: BrushBounds,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BrushCsgOp { enum BrushCsgOp {
Intersect, Intersect,
Merge, Merge,
@ -162,13 +186,75 @@ fn run_bounds_csg(world: &mut World, op: BrushCsgOp) {
return; return;
} }
apply_bounds_result(world, selected[0], result, new_brush); world.resource_mut::<BrushCsgPreview>().pending = Some(PendingBrushCsg {
if matches!(op, BrushCsgOp::Merge | BrushCsgOp::Intersect) { op,
delete_entities_with_history(world, &selected[1..]); selected,
} result,
});
set_status( set_status(
world, world,
match op { match op {
BrushCsgOp::Intersect => "Brush intersect preview: Enter commits, Esc cancels",
BrushCsgOp::Merge => "Brush convex merge preview: Enter commits, Esc cancels",
BrushCsgOp::Subtract => "Brush subtract preview: Enter commits, Esc cancels",
},
);
}
fn brush_csg_preview_input(world: &mut World) {
let enter = world
.get_resource::<ButtonInput<KeyCode>>()
.is_some_and(|keys| keys.just_pressed(KeyCode::Enter));
let escape = world
.get_resource::<ButtonInput<KeyCode>>()
.is_some_and(|keys| keys.just_pressed(KeyCode::Escape));
if !enter && !escape {
return;
}
if escape {
if world
.resource_mut::<BrushCsgPreview>()
.pending
.take()
.is_some()
{
set_status(world, "Brush CSG preview canceled");
}
return;
}
let Some(pending) = world.resource_mut::<BrushCsgPreview>().pending.take() else {
return;
};
commit_csg_preview(world, pending);
}
fn commit_csg_preview(world: &mut World, pending: PendingBrushCsg) {
let Some(primary) = pending.selected.first().copied() else {
set_status(world, "Brush CSG commit failed: missing primary brush");
return;
};
if world.get::<BrushDesc>(primary).is_none() {
set_status(
world,
"Brush CSG commit failed: primary brush no longer exists",
);
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
.selected
.iter()
.skip(1)
.copied()
.filter(|entity| world.get::<BrushDesc>(*entity).is_some())
.collect();
delete_entities_with_history(world, &to_delete);
}
set_status(
world,
match pending.op {
BrushCsgOp::Intersect => "Brush intersect committed", BrushCsgOp::Intersect => "Brush intersect committed",
BrushCsgOp::Merge => "Brush convex merge committed", BrushCsgOp::Merge => "Brush convex merge committed",
BrushCsgOp::Subtract => "Brush subtract committed", BrushCsgOp::Subtract => "Brush subtract committed",
@ -176,6 +262,48 @@ fn run_bounds_csg(world: &mut World, op: BrushCsgOp) {
); );
} }
fn draw_brush_csg_preview(preview: Res<BrushCsgPreview>, mut gizmos: Gizmos) {
let Some(pending) = preview.pending.as_ref() else {
return;
};
draw_bounds_box(
&mut gizmos,
pending.result,
Color::srgba(0.25, 0.85, 1.0, 0.95),
);
}
fn draw_bounds_box(gizmos: &mut Gizmos, bounds: BrushBounds, color: Color) {
let min = bounds.min;
let max = bounds.max;
let corners = [
Vec3::new(min.x, min.y, min.z),
Vec3::new(max.x, min.y, min.z),
Vec3::new(max.x, max.y, min.z),
Vec3::new(min.x, max.y, min.z),
Vec3::new(min.x, min.y, max.z),
Vec3::new(max.x, min.y, max.z),
Vec3::new(max.x, max.y, max.z),
Vec3::new(min.x, max.y, max.z),
];
for (a, b) in [
(0, 1),
(1, 2),
(2, 3),
(3, 0),
(4, 5),
(5, 6),
(6, 7),
(7, 4),
(0, 4),
(1, 5),
(2, 6),
(3, 7),
] {
gizmos.line(corners[a], corners[b], color);
}
}
fn first_brush_error(report: &shared::brush_math::BrushValidationReport) -> &str { fn first_brush_error(report: &shared::brush_math::BrushValidationReport) -> &str {
report report
.diagnostics .diagnostics

View File

@ -17,7 +17,7 @@ pub mod visualizers;
pub use actor_icons::ActorIconsPlugin; pub use actor_icons::ActorIconsPlugin;
pub use brush_csg::{ pub use brush_csg::{
intersect_selected_brushes, merge_selected_brushes, selected_brush_count, intersect_selected_brushes, merge_selected_brushes, selected_brush_count,
subtract_selected_brushes, subtract_selected_brushes, BrushCsgPlugin,
}; };
pub use brush_edit::{BrushEditMode, BrushEditPlugin, BrushElementSelection}; pub use brush_edit::{BrushEditMode, BrushEditPlugin, BrushElementSelection};
pub use brush_tool::{BrushToolPlugin, BrushToolState}; pub use brush_tool::{BrushToolPlugin, BrushToolState};

View File

@ -30,7 +30,7 @@ The command palette exposes `brush.subtract`, `brush.intersect`, and `brush.merg
- **Convex Merge** replaces the primary selected brush with the bounding cuboid union and deletes the other selected brushes. - **Convex Merge** replaces the primary selected brush with the bounding cuboid union and deletes the other selected brushes.
- **Subtract** clips the primary selected brush to the largest remaining axis-aligned slab after subtracting the second selected brush; the cutter remains in the scene. - **Subtract** clips the primary selected brush to the largest remaining axis-aligned slab after subtracting the second selected brush; the cutter remains in the scene.
These operations validate selected brushes before applying results and validate the generated result before mutating or deleting scene actors. Failed operations report status text and leave the scene untouched. Successful operations commit through history but are not full arbitrary-face CSG yet. Full convex clipping, multi-output subtraction, and material-preserving face matching remain future work. These operations validate selected brushes before previewing results and validate the generated result before any scene mutation. A cyan wireframe preview shows the pending bounds result; **Enter** commits and **Esc** cancels. Failed operations report status text and leave the scene untouched. Successful operations commit through history but are not full arbitrary-face CSG yet. Full convex clipping, multi-output subtraction, and material-preserving face matching remain future work.
## Current Limits ## Current Limits