//! Brush element edit modes and viewport element picking. use bevy::prelude::*; use bevy_egui::{egui, EguiContexts}; use shared::{BrushDesc, BrushFaceDesc, BrushPlaneDesc, ComponentInstanceId}; use crate::camera::EditorCamera; use crate::history::{push_command, EditorCommand}; use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus}; use crate::scene_io::SceneIo; use crate::selection::ViewportClick; use crate::state::scene_tools_active; use crate::ui::{viewport_keyboard_shortcuts_active, UiState}; use crate::viewport::{scene_view_ray, ViewportDisplayMode}; #[derive(Resource, Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum BrushEditMode { #[default] Object, Vertex, Edge, Face, Clip, } impl BrushEditMode { pub fn label(self) -> &'static str { match self { Self::Object => "Object", Self::Vertex => "Vertex", Self::Edge => "Edge", Self::Face => "Face", Self::Clip => "Clip", } } pub fn is_element_mode(self) -> bool { self != Self::Object } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum BrushElementKey { Vertex { face: ComponentInstanceId, index: usize, }, Edge { face: ComponentInstanceId, index: usize, }, Face { face: ComponentInstanceId, }, } #[derive(Resource, Default, Debug, Clone)] pub struct BrushElementSelection { pub brush: Option, pub elements: Vec, } impl BrushElementSelection { fn replace(&mut self, brush: Entity, element: BrushElementKey) { self.brush = Some(brush); self.elements.clear(); self.elements.push(element); } fn toggle(&mut self, brush: Entity, element: BrushElementKey) { if self.brush != Some(brush) { self.replace(brush, element); return; } if let Some(index) = self .elements .iter() .position(|existing| existing == &element) { self.elements.remove(index); } else { self.elements.push(element); } } pub fn clear(&mut self) { self.brush = None; self.elements.clear(); } } pub struct BrushEditPlugin; impl Plugin for BrushEditPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .init_resource::() .add_systems( Update, ( brush_edit_hotkeys, brush_element_pick, brush_element_drag, draw_brush_edit_overlays, ) .chain() .run_if(scene_tools_active), ); } } #[derive(Resource, Default, Debug, Clone)] struct BrushElementDrag { brush: Option, old: Option, last_floor: Option, changed: bool, } #[allow(clippy::too_many_arguments)] fn brush_edit_hotkeys( mut mode: ResMut, mut element_selection: ResMut, mut active_operator: ResMut, mut scene_io: ResMut, keys: Res>, buttons: Res>, mut contexts: EguiContexts, ui_state: Res, display: Res, brushes: Query<(), With>, ) -> Result { if display.clean_game_view { return Ok(()); } let ctx = contexts.ctx_mut()?; if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) || ctx.wants_keyboard_input() { return Ok(()); } if keys.just_pressed(KeyCode::Escape) && mode.is_element_mode() { *mode = BrushEditMode::Object; element_selection.clear(); set_brush_edit_status(&mut active_operator, OperatorPhase::Canceled, "Object mode"); scene_io.status = "Brush edit: Object mode".to_string(); return Ok(()); } let requested = if keys.just_pressed(KeyCode::Digit1) { Some(BrushEditMode::Vertex) } else if keys.just_pressed(KeyCode::Digit2) { Some(BrushEditMode::Edge) } else if keys.just_pressed(KeyCode::Digit3) { Some(BrushEditMode::Face) } else if keys.just_pressed(KeyCode::Digit4) { Some(BrushEditMode::Clip) } else { None }; let Some(requested) = requested else { return Ok(()); }; let Some(selected_brush) = selected_brush_entity(&ui_state, &brushes) else { scene_io.status = "Select a brush before entering brush edit mode".to_string(); set_brush_edit_status( &mut active_operator, OperatorPhase::Blocked, "Select a brush before editing elements", ); return Ok(()); }; *mode = requested; element_selection.brush = Some(selected_brush); element_selection.elements.clear(); scene_io.status = format!("Brush edit: {} mode", mode.label()); set_brush_edit_status( &mut active_operator, OperatorPhase::Preview, format!("{} mode", mode.label()), ); Ok(()) } #[allow(clippy::too_many_arguments)] fn brush_element_pick( mode: Res, mut viewport_click: ResMut, mut element_selection: ResMut, keys: Res>, ui_state: Res, cameras: Query<(&Camera, &GlobalTransform), With>, brushes: Query<(Entity, &BrushDesc, &GlobalTransform)>, ) { if !mode.is_element_mode() { return; } let Some(pointer_pos) = viewport_click.0.take() else { return; }; let Some(ray) = viewport_ray(&cameras, pointer_pos, ui_state.viewport_rect) else { return; }; let brush_filter = element_selection.brush; let mut best: Option<(Entity, BrushElementKey, f32)> = None; for (entity, brush, transform) in &brushes { if brush_filter.is_some_and(|selected| selected != entity) { continue; } if let Some((element, distance)) = pick_element(*mode, brush, transform, ray) { if best .as_ref() .is_none_or(|(_, _, best_distance)| distance < *best_distance) { best = Some((entity, element, distance)); } } } let Some((brush, element, _)) = best else { if !keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) { element_selection.clear(); } return; }; if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) { element_selection.toggle(brush, element); } else { element_selection.replace(brush, element); } } #[allow(clippy::too_many_arguments)] fn brush_element_drag( mut commands: Commands, mode: Res, selection: Res, mut drag: ResMut, mut active_operator: ResMut, buttons: Res>, ui_state: Res, cameras: Query<(&Camera, &GlobalTransform), With>, mut brushes: Query<(&mut BrushDesc, &GlobalTransform)>, ) { if !mode.is_element_mode() || matches!(*mode, BrushEditMode::Clip) { clear_drag(&mut drag); return; } let Some(brush_entity) = selection.brush else { clear_drag(&mut drag); return; }; if selection.elements.is_empty() { clear_drag(&mut drag); return; } let Some(pointer_pos) = ui_state.viewport_pointer_pos else { return; }; let Some(current_floor) = pointer_floor_position(&cameras, pointer_pos, ui_state.viewport_rect) else { return; }; if buttons.just_pressed(MouseButton::Left) && drag.brush.is_none() { if let Ok((brush, _)) = brushes.get(brush_entity) { drag.brush = Some(brush_entity); drag.old = Some(brush.clone()); drag.last_floor = Some(current_floor); drag.changed = false; } return; } if buttons.pressed(MouseButton::Left) && drag.brush == Some(brush_entity) { let Some(last_floor) = drag.last_floor else { drag.last_floor = Some(current_floor); return; }; let world_delta = current_floor - last_floor; if world_delta.length_squared() <= 0.000001 { return; } if let Ok((mut brush, transform)) = brushes.get_mut(brush_entity) { let local_delta = transform.affine().inverse().transform_vector3(world_delta); if move_selected_elements(&mut brush, &selection.elements, local_delta) { drag.changed = true; drag.last_floor = Some(current_floor); set_brush_edit_status( &mut active_operator, OperatorPhase::Preview, format!("Dragging {} element(s)", selection.elements.len()), ); } } } if buttons.just_released(MouseButton::Left) { if let (Some(brush), Some(old)) = (drag.brush, drag.old.take()) { if drag.changed { if let Ok((brush_desc, _)) = brushes.get(brush) { let new = brush_desc.clone(); commands.queue(move |world: &mut World| { push_command( world, EditorCommand::SetBrush { entity: brush, old: Some(old), new, }, ); }); set_brush_edit_status( &mut active_operator, OperatorPhase::Committed, "Committed brush element edit", ); } } } clear_drag(&mut drag); } } fn clear_drag(drag: &mut BrushElementDrag) { drag.brush = None; drag.old = None; drag.last_floor = None; drag.changed = false; } fn draw_brush_edit_overlays( mode: Res, selection: Res, ui_state: Res, brushes: Query<(Entity, &BrushDesc, &GlobalTransform)>, mut gizmos: Gizmos, ) { if !mode.is_element_mode() { return; } let selected_brushes: Vec = ui_state.selected_entities.iter().collect(); for (entity, brush, transform) in &brushes { if !selected_brushes.contains(&entity) { continue; } let active = selection.brush == Some(entity); draw_brush_elements( *mode, brush, transform, &selection.elements, active, &mut gizmos, ); } } fn draw_brush_elements( mode: BrushEditMode, brush: &BrushDesc, transform: &GlobalTransform, selected: &[BrushElementKey], active: bool, gizmos: &mut Gizmos, ) { let base_color = match mode { BrushEditMode::Vertex => Color::srgb(0.2, 0.72, 1.0), BrushEditMode::Edge => Color::srgb(1.0, 0.68, 0.24), BrushEditMode::Face => Color::srgb(0.24, 1.0, 0.56), BrushEditMode::Clip => Color::srgb(0.92, 0.36, 1.0), BrushEditMode::Object => Color::WHITE, }; let selected_color = Color::srgb(1.0, 0.95, 0.2); for face in &brush.faces { let vertices = world_vertices(face, transform); if vertices.is_empty() { continue; } let face_center = average_point(&vertices); let normal = transform .affine() .transform_vector3(face.plane.normal) .normalize_or_zero(); if matches!(mode, BrushEditMode::Face | BrushEditMode::Clip) { let key = BrushElementKey::Face { face: face.id.clone(), }; let color = if active && selected.contains(&key) { selected_color } else { base_color }; gizmos.sphere(face_center, 0.08, color); gizmos.line(face_center, face_center + normal * 0.55, color); } for index in 0..vertices.len() { let next = (index + 1) % vertices.len(); if matches!(mode, BrushEditMode::Vertex) { let key = BrushElementKey::Vertex { face: face.id.clone(), index, }; let color = if active && selected.contains(&key) { selected_color } else { base_color }; gizmos.sphere(vertices[index], 0.06, color); } if matches!(mode, BrushEditMode::Edge | BrushEditMode::Clip) { let key = BrushElementKey::Edge { face: face.id.clone(), index, }; let color = if active && selected.contains(&key) { selected_color } else { base_color }; gizmos.line(vertices[index], vertices[next], color); } } } } fn selected_brush_entity( ui_state: &UiState, brushes: &Query<(), With>, ) -> Option { ui_state .selected_entities .iter() .find(|entity| brushes.contains(*entity)) } fn viewport_ray( cameras: &Query<(&Camera, &GlobalTransform), With>, pointer_pos: egui::Pos2, scene_rect: egui::Rect, ) -> Option { let (camera, transform) = cameras .iter() .find(|(camera, _)| camera.is_active) .or_else(|| cameras.iter().next())?; scene_view_ray(camera, transform, pointer_pos, scene_rect) } fn pick_element( mode: BrushEditMode, brush: &BrushDesc, transform: &GlobalTransform, ray: Ray3d, ) -> Option<(BrushElementKey, f32)> { match mode { BrushEditMode::Vertex => pick_vertex(brush, transform, ray), BrushEditMode::Edge => pick_edge(brush, transform, ray), BrushEditMode::Face | BrushEditMode::Clip => pick_face(brush, transform, ray), BrushEditMode::Object => None, } } fn pointer_floor_position( cameras: &Query<(&Camera, &GlobalTransform), With>, pointer_pos: egui::Pos2, scene_rect: egui::Rect, ) -> Option { let ray = viewport_ray(cameras, pointer_pos, scene_rect)?; let direction = ray.direction.as_vec3(); if direction.y.abs() < 0.0001 { return Some(ray.origin); } let t = -ray.origin.y / direction.y; Some(ray.origin + direction * t.max(0.0)) } fn move_selected_elements( brush: &mut BrushDesc, selected: &[BrushElementKey], delta: Vec3, ) -> bool { if delta.length_squared() <= 0.000001 { return false; } let anchors = selected_anchor_positions(brush, selected); if anchors.is_empty() { return false; } let mut changed = false; for face in &mut brush.faces { for vertex in &mut face.vertices { if anchors .iter() .any(|anchor| vertex.distance_squared(*anchor) <= 0.0001) { *vertex += delta; changed = true; } } } if changed { recompute_face_planes(brush); } changed } fn selected_anchor_positions(brush: &BrushDesc, selected: &[BrushElementKey]) -> Vec { let mut anchors = Vec::new(); for element in selected { match element { BrushElementKey::Vertex { face, index } => { if let Some(face_desc) = brush.faces.iter().find(|candidate| &candidate.id == face) { if let Some(vertex) = face_desc.vertices.get(*index) { push_unique_anchor(&mut anchors, *vertex); } } } BrushElementKey::Edge { face, index } => { if let Some(face_desc) = brush.faces.iter().find(|candidate| &candidate.id == face) { if !face_desc.vertices.is_empty() { let next = (*index + 1) % face_desc.vertices.len(); if let Some(vertex) = face_desc.vertices.get(*index) { push_unique_anchor(&mut anchors, *vertex); } if let Some(vertex) = face_desc.vertices.get(next) { push_unique_anchor(&mut anchors, *vertex); } } } } BrushElementKey::Face { face } => { if let Some(face_desc) = brush.faces.iter().find(|candidate| &candidate.id == face) { for vertex in &face_desc.vertices { push_unique_anchor(&mut anchors, *vertex); } } } } } anchors } fn push_unique_anchor(anchors: &mut Vec, vertex: Vec3) { if !anchors .iter() .any(|existing| existing.distance_squared(vertex) <= 0.0001) { anchors.push(vertex); } } fn recompute_face_planes(brush: &mut BrushDesc) { for face in &mut brush.faces { let Some(plane) = plane_from_vertices(&face.vertices, face.plane.normal) else { continue; }; face.plane = plane; } } fn plane_from_vertices(vertices: &[Vec3], fallback_normal: Vec3) -> Option { if vertices.len() < 3 { return None; } let normal = (vertices[1] - vertices[0]) .cross(vertices[2] - vertices[0]) .try_normalize() .or_else(|| fallback_normal.try_normalize())?; Some(BrushPlaneDesc { normal, distance: normal.dot(vertices[0]), }) } fn pick_vertex( brush: &BrushDesc, transform: &GlobalTransform, ray: Ray3d, ) -> Option<(BrushElementKey, f32)> { let mut best = None; for face in &brush.faces { for (index, vertex) in world_vertices(face, transform).iter().enumerate() { let distance = distance_ray_point(ray, *vertex); if distance < 0.25 && best.as_ref().is_none_or(|(_, best)| distance < *best) { best = Some(( BrushElementKey::Vertex { face: face.id.clone(), index, }, distance, )); } } } best } fn pick_edge( brush: &BrushDesc, transform: &GlobalTransform, ray: Ray3d, ) -> Option<(BrushElementKey, f32)> { let mut best = None; for face in &brush.faces { let vertices = world_vertices(face, transform); for index in 0..vertices.len() { let next = (index + 1) % vertices.len(); let distance = distance_ray_segment(ray, vertices[index], vertices[next]); if distance < 0.2 && best.as_ref().is_none_or(|(_, best)| distance < *best) { best = Some(( BrushElementKey::Edge { face: face.id.clone(), index, }, distance, )); } } } best } fn pick_face( brush: &BrushDesc, transform: &GlobalTransform, ray: Ray3d, ) -> Option<(BrushElementKey, f32)> { let mut best = None; for face in &brush.faces { let vertices = world_vertices(face, transform); if vertices.len() < 3 { continue; } let normal = transform .affine() .transform_vector3(face.plane.normal) .normalize_or_zero(); let denom = normal.dot(ray.direction.as_vec3()); if denom.abs() < 0.0001 { continue; } let distance = normal.dot(vertices[0] - ray.origin) / denom; if distance < 0.0 { continue; } let hit = ray.origin + ray.direction.as_vec3() * distance; if point_in_convex_polygon(hit, &vertices, normal) && best.as_ref().is_none_or(|(_, best)| distance < *best) { best = Some(( BrushElementKey::Face { face: face.id.clone(), }, distance, )); } } best } fn world_vertices(face: &BrushFaceDesc, transform: &GlobalTransform) -> Vec { face.vertices .iter() .map(|vertex| transform.transform_point(*vertex)) .collect() } fn average_point(points: &[Vec3]) -> Vec3 { points.iter().copied().sum::() / points.len() as f32 } fn point_in_convex_polygon(point: Vec3, vertices: &[Vec3], normal: Vec3) -> bool { let mut expected_sign = 0.0f32; for index in 0..vertices.len() { let next = (index + 1) % vertices.len(); let edge = vertices[next] - vertices[index]; let to_point = point - vertices[index]; let side = edge.cross(to_point).dot(normal); if side.abs() <= 0.0001 { continue; } if expected_sign == 0.0 { expected_sign = side.signum(); } else if side.signum() != expected_sign { return false; } } true } fn distance_ray_point(ray: Ray3d, point: Vec3) -> f32 { let to_point = point - ray.origin; let t = to_point.dot(ray.direction.as_vec3()).max(0.0); (ray.origin + ray.direction.as_vec3() * t - point).length() } fn distance_ray_segment(ray: Ray3d, a: Vec3, b: Vec3) -> f32 { let ray_dir = ray.direction.as_vec3(); let segment = b - a; let w0 = ray.origin - a; let a_dot = ray_dir.dot(ray_dir); let b_dot = ray_dir.dot(segment); let c_dot = segment.dot(segment); let d_dot = ray_dir.dot(w0); let e_dot = segment.dot(w0); let denom = a_dot * c_dot - b_dot * b_dot; let ray_t = if denom.abs() > 0.0001 { ((b_dot * e_dot - c_dot * d_dot) / denom).max(0.0) } else { 0.0 }; let segment_t = if c_dot > 0.0001 { ((a_dot * e_dot - b_dot * d_dot) / denom.max(0.0001)).clamp(0.0, 1.0) } else { 0.0 }; let ray_point = ray.origin + ray_dir * ray_t; let segment_point = a + segment * segment_t; ray_point.distance(segment_point) } fn set_brush_edit_status( active_operator: &mut ActiveOperator, phase: OperatorPhase, hint: impl Into, ) { active_operator.status = Some(OperatorStatus { id: "brush.edit_mode".to_string(), label: "Brush Edit".to_string(), phase, hint: hint.into(), warnings: Vec::new(), }); } #[cfg(test)] mod tests { use super::*; use shared::BrushDesc; #[test] fn point_inside_convex_polygon_is_detected() { let vertices = vec![ Vec3::new(-1.0, 0.0, -1.0), Vec3::new(1.0, 0.0, -1.0), Vec3::new(1.0, 0.0, 1.0), Vec3::new(-1.0, 0.0, 1.0), ]; assert!(point_in_convex_polygon(Vec3::ZERO, &vertices, Vec3::Y)); assert!(!point_in_convex_polygon( Vec3::new(2.0, 0.0, 0.0), &vertices, Vec3::Y )); } #[test] fn moving_selected_vertex_moves_matching_welded_corners() { let mut brush = BrushDesc::default(); let selected_vertex = brush.faces[0].vertices[0]; let before = brush .faces .iter() .flat_map(|face| face.vertices.iter()) .filter(|vertex| vertex.distance_squared(selected_vertex) <= 0.0001) .count(); let face = brush.faces[0].id.clone(); let changed = move_selected_elements( &mut brush, &[BrushElementKey::Vertex { face, index: 0 }], Vec3::new(0.5, 0.0, 0.0), ); let after = brush .faces .iter() .flat_map(|face| face.vertices.iter()) .filter(|vertex| vertex.distance_squared(selected_vertex + Vec3::X * 0.5) <= 0.0001) .count(); assert!(changed); assert_eq!(before, after); } }