Add brush face controls
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run

This commit is contained in:
Rbanh 2026-06-06 06:09:02 -04:00
parent 02beeb7085
commit d847706b8f
2 changed files with 116 additions and 0 deletions

View File

@ -33,6 +33,7 @@ use crate::ui::theme::{
WIDGET_BG, WIDGET_BG,
}; };
use crate::ui::widgets::{icon_button_small, phosphor_icon, phosphor_icon_text}; use crate::ui::widgets::{icon_button_small, phosphor_icon, phosphor_icon_text};
use crate::viewport::brush_edit::{BrushElementKey, BrushElementSelection};
use super::component_registry::{ use super::component_registry::{
EditorComponentCategory, EditorComponentDescriptor, EditorComponentRegistry, EditorComponentCategory, EditorComponentDescriptor, EditorComponentRegistry,
@ -2440,6 +2441,7 @@ fn brush_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
changed |= ui.checkbox(&mut brush.receive_shadows, "Receive").changed(); changed |= ui.checkbox(&mut brush.receive_shadows, "Receive").changed();
}); });
}); });
changed |= selected_brush_face_controls(world, ui, entity, &mut brush);
if ui.button("Reset Cube Brush").clicked() { if ui.button("Reset Cube Brush").clicked() {
reset_cube = true; reset_cube = true;
} }
@ -2455,6 +2457,117 @@ fn brush_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
} }
} }
fn selected_brush_face_controls(
world: &World,
ui: &mut egui::Ui,
entity: Entity,
brush: &mut BrushDesc,
) -> bool {
let Some(selection) = world.get_resource::<BrushElementSelection>() else {
return false;
};
if selection.brush != Some(entity) {
return false;
}
let selected_faces: Vec<_> = selection
.elements
.iter()
.filter_map(|element| match element {
BrushElementKey::Face { face } => Some(face.clone()),
_ => None,
})
.collect();
if selected_faces.is_empty() {
return false;
}
let mut changed = false;
ui.add_space(6.0);
ui.separator();
ui.label(
egui::RichText::new(format!("{} selected face(s)", selected_faces.len())).color(TEXT_DIM),
);
for face_id in selected_faces {
let Some(face) = brush.faces.iter_mut().find(|face| face.id == face_id) else {
continue;
};
let label = if face.id.0.trim().is_empty() {
"Face".to_string()
} else {
face.id.0.clone()
};
egui::CollapsingHeader::new(label)
.default_open(true)
.show(ui, |ui| {
property_row(ui, "UV offset", |ui| {
changed |= ui
.add(egui::DragValue::new(&mut face.uv_offset.x).speed(0.05))
.changed();
changed |= ui
.add(egui::DragValue::new(&mut face.uv_offset.y).speed(0.05))
.changed();
});
property_row(ui, "UV scale", |ui| {
changed |= ui
.add(egui::DragValue::new(&mut face.uv_scale.x).speed(0.05))
.changed();
changed |= ui
.add(egui::DragValue::new(&mut face.uv_scale.y).speed(0.05))
.changed();
});
property_row(ui, "UV rotation", |ui| {
changed |= ui
.add(egui::DragValue::new(&mut face.uv_rotation).speed(1.0))
.changed();
});
changed |= editor_asset_ref_option_ui(ui, "Material", &mut face.material);
changed |= editor_asset_ref_option_ui(ui, "Texture", &mut face.texture);
});
}
changed
}
fn editor_asset_ref_option_ui(
ui: &mut egui::Ui,
label: &str,
value: &mut Option<EditorAssetRef>,
) -> bool {
let mut changed = false;
property_row(ui, label, |ui| {
let mut enabled = value.is_some();
if ui.checkbox(&mut enabled, "").changed() {
if enabled {
*value = Some(EditorAssetRef::default());
} else {
*value = None;
}
changed = true;
}
if let Some(reference) = value.as_mut() {
ui.vertical(|ui| {
ui.set_min_width(MIN_INLINE_CONTROL_WIDTH);
changed |= ui
.text_edit_singleline(&mut reference.label)
.on_hover_text("Cached display label")
.changed();
changed |= ui
.text_edit_singleline(&mut reference.asset_id)
.on_hover_text("Asset registry ID")
.changed();
changed |= ui
.text_edit_singleline(&mut reference.sub_asset_id)
.on_hover_text("Imported sub-asset ID")
.changed();
});
} else {
ui.label(egui::RichText::new("(none)").color(TEXT_DIM));
}
});
changed
}
fn primitive_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { fn primitive_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
let Some(mut primitive) = world.get::<Primitive>(entity).cloned() else { let Some(mut primitive) = world.get::<Primitive>(entity).cloned() else {
return; return;

View File

@ -17,6 +17,8 @@ With a brush actor selected, `1`/`2`/`3`/`4` enter vertex, edge, face, and clip
Vertex, edge, and face selections can be dragged on the viewport floor plane. The editor moves matching duplicated corner vertices across all brush faces so simple cube/prism brushes stay welded, recomputes face planes, and commits one undoable brush edit when the drag releases. Clip mode currently provides element visualization/selection only; split application is future CSG work. Vertex, edge, and face selections can be dragged on the viewport floor plane. The editor moves matching duplicated corner vertices across all brush faces so simple cube/prism brushes stay welded, recomputes face planes, and commits one undoable brush edit when the drag releases. Clip mode currently provides element visualization/selection only; split application is future CSG work.
When one or more faces are selected in Face mode, the Brush inspector shows selected-face controls for UV offset, UV scale, UV rotation, material ref, and texture ref. These fields edit the persisted `BrushFaceDesc` data through undoable brush updates. The current controls are data-oriented; Asset Browser picker/drop polish and per-face material batching in hydration are still follow-up work.
## Boolean Operations ## Boolean Operations
The command palette exposes `brush.subtract`, `brush.intersect`, and `brush.merge_convex` for selected brush actors. The current implementation is a conservative bounds-based MVP intended for cuboid/prism blockout brushes: The command palette exposes `brush.subtract`, `brush.intersect`, and `brush.merge_convex` for selected brush actors. The current implementation is a conservative bounds-based MVP intended for cuboid/prism blockout brushes:
@ -32,3 +34,4 @@ These operations commit through history but are not full arbitrary-face CSG yet.
- Only convex authored faces are supported by the mesh builder. - Only convex authored faces are supported by the mesh builder.
- Subtractive brush markers are stored but not automatically evaluated. - Subtractive brush markers are stored but not automatically evaluated.
- Clip split application and arbitrary-face CSG remain future roadmap work. - Clip split application and arbitrary-face CSG remain future roadmap work.
- Per-face material and texture refs persist, but generated brush meshes still hydrate through a single material batch.