use rand::Rng;
use rapier2d::prelude::*;
use super::ascii_art::get_ascii_art;
use super::colliders::{create_shape_collider, remove_shape_collider, shapes_overlap};
use super::types::{Shape, ShapeType};
const MIN_SHAPE_SEPARATION: f32 = 4.0;
const EDGE_MARGIN: f32 = 3.0;
#[derive(Debug)]
pub struct ShapeManager {
shapes: Vec<Shape>,
next_id: u32,
selected_id: Option<u32>,
}
impl Default for ShapeManager {
fn default() -> Self {
Self::new()
}
}
impl ShapeManager {
pub fn new() -> Self {
Self {
shapes: Vec::new(),
next_id: 1,
selected_id: None,
}
}
pub fn shape_count(&self) -> usize {
self.shapes.len()
}
pub fn shapes(&self) -> impl Iterator<Item = &Shape> {
self.shapes.iter()
}
pub fn shapes_mut(&mut self) -> impl Iterator<Item = &mut Shape> {
self.shapes.iter_mut()
}
pub fn selected_shape(&self) -> Option<&Shape> {
self.selected_id
.and_then(|id| self.shapes.iter().find(|s| s.id() == id))
}
pub fn selected_shape_mut(&mut self) -> Option<&mut Shape> {
let selected_id = self.selected_id?;
self.shapes.iter_mut().find(|s| s.id() == selected_id)
}
pub fn selected_id(&self) -> Option<u32> {
self.selected_id
}
pub fn add_shape(
&mut self,
shape_type: ShapeType,
x: f32,
y: f32,
rigid_body_set: &mut RigidBodySet,
collider_set: &mut ColliderSet,
) -> u32 {
let id = self.next_id;
self.next_id += 1;
let mut shape = Shape::new(id, shape_type, x, y);
let (body_handle, collider_handle) =
create_shape_collider(&shape, rigid_body_set, collider_set);
shape.set_physics_handles(body_handle, collider_handle);
self.shapes.push(shape);
id
}
#[allow(clippy::too_many_arguments)]
pub fn remove_shape(
&mut self,
id: u32,
rigid_body_set: &mut RigidBodySet,
collider_set: &mut ColliderSet,
island_manager: &mut IslandManager,
impulse_joint_set: &mut ImpulseJointSet,
multibody_joint_set: &mut MultibodyJointSet,
) -> bool {
if let Some(idx) = self.shapes.iter().position(|s| s.id() == id) {
let shape = &self.shapes[idx];
remove_shape_collider(
shape,
rigid_body_set,
collider_set,
island_manager,
impulse_joint_set,
multibody_joint_set,
);
if self.selected_id == Some(id) {
self.selected_id = None;
}
self.shapes.remove(idx);
true
} else {
false
}
}
pub fn clear_all(
&mut self,
rigid_body_set: &mut RigidBodySet,
collider_set: &mut ColliderSet,
island_manager: &mut IslandManager,
impulse_joint_set: &mut ImpulseJointSet,
multibody_joint_set: &mut MultibodyJointSet,
) {
for shape in &self.shapes {
remove_shape_collider(
shape,
rigid_body_set,
collider_set,
island_manager,
impulse_joint_set,
multibody_joint_set,
);
}
self.shapes.clear();
self.selected_id = None;
}
pub fn select_at(&mut self, x: f32, y: f32, collider_set: &ColliderSet) -> Option<u32> {
if let Some(old_id) = self.selected_id {
if let Some(shape) = self.shapes.iter_mut().find(|s| s.id() == old_id) {
shape.set_selected(false);
}
}
let mut best_match: Option<(u32, f32)> = None;
for shape in &self.shapes {
let (cx, cy) = shape.position();
let dx = x - cx;
let dy = y - cy;
let dist_sq = dx * dx + dy * dy;
let ascii_art = get_ascii_art(shape.shape_type());
let hit_radius = (ascii_art.width().max(ascii_art.height()) as f32 / 2.0) + 1.0;
if dist_sq < hit_radius * hit_radius {
match best_match {
Some((_, best_dist)) if dist_sq < best_dist => {
best_match = Some((shape.id(), dist_sq));
}
None => {
best_match = Some((shape.id(), dist_sq));
}
_ => {}
}
}
}
for shape in &self.shapes {
if let Some(collider_handle) = shape.collider_handle() {
if let Some(collider) = collider_set.get(collider_handle) {
let point = point![x, y];
let local_point = collider.position().inverse_transform_point(&point);
if collider.shape().contains_local_point(&local_point) {
let (cx, cy) = shape.position();
let dx = x - cx;
let dy = y - cy;
let dist_sq = dx * dx + dy * dy;
match best_match {
Some((_, best_dist)) if dist_sq < best_dist => {
best_match = Some((shape.id(), dist_sq));
}
None => {
best_match = Some((shape.id(), dist_sq));
}
_ => {}
}
}
}
}
}
if let Some((id, _)) = best_match {
if let Some(shape) = self.shapes.iter_mut().find(|s| s.id() == id) {
shape.set_selected(true);
}
self.selected_id = Some(id);
Some(id)
} else {
self.selected_id = None;
None
}
}
pub fn deselect(&mut self) {
if let Some(id) = self.selected_id {
if let Some(shape) = self.shapes.iter_mut().find(|s| s.id() == id) {
shape.set_selected(false);
}
}
self.selected_id = None;
}
pub fn rotate_selected_clockwise(&mut self, rigid_body_set: &mut RigidBodySet) -> bool {
if let Some(shape) = self.selected_shape_mut() {
shape.rotate_clockwise();
if let Some(handle) = shape.rigid_body_handle() {
if let Some(body) = rigid_body_set.get_mut(handle) {
let (x, y) = shape.position();
let rotation = shape.rotation_radians();
let isometry = Isometry::new(vector![x, y], rotation);
body.set_position(isometry, true);
}
}
true
} else {
false
}
}
pub fn rotate_selected_counter_clockwise(&mut self, rigid_body_set: &mut RigidBodySet) -> bool {
if let Some(shape) = self.selected_shape_mut() {
shape.rotate_counter_clockwise();
if let Some(handle) = shape.rigid_body_handle() {
if let Some(body) = rigid_body_set.get_mut(handle) {
let (x, y) = shape.position();
let rotation = shape.rotation_radians();
let isometry = Isometry::new(vector![x, y], rotation);
body.set_position(isometry, true);
}
}
true
} else {
false
}
}
pub fn move_selected(&mut self, dx: f32, dy: f32, rigid_body_set: &mut RigidBodySet) -> bool {
if let Some(shape) = self.selected_shape_mut() {
let (x, y) = shape.position();
shape.set_position(x + dx, y + dy);
if let Some(handle) = shape.rigid_body_handle() {
if let Some(body) = rigid_body_set.get_mut(handle) {
let (new_x, new_y) = shape.position();
let rotation = shape.rotation_radians();
let isometry = Isometry::new(vector![new_x, new_y], rotation);
body.set_position(isometry, true);
}
}
true
} else {
false
}
}
pub fn move_selected_to(&mut self, x: f32, y: f32, rigid_body_set: &mut RigidBodySet) -> bool {
if let Some(shape) = self.selected_shape_mut() {
shape.set_position(x, y);
if let Some(handle) = shape.rigid_body_handle() {
if let Some(body) = rigid_body_set.get_mut(handle) {
let rotation = shape.rotation_radians();
let isometry = Isometry::new(vector![x, y], rotation);
body.set_position(isometry, true);
}
}
true
} else {
false
}
}
pub fn cycle_selected_color_forward(&mut self) -> bool {
if let Some(shape) = self.selected_shape_mut() {
shape.cycle_color_forward();
true
} else {
false
}
}
pub fn cycle_selected_color_backward(&mut self) -> bool {
if let Some(shape) = self.selected_shape_mut() {
shape.cycle_color_backward();
true
} else {
false
}
}
#[allow(clippy::too_many_arguments)]
pub fn remove_selected(
&mut self,
rigid_body_set: &mut RigidBodySet,
collider_set: &mut ColliderSet,
island_manager: &mut IslandManager,
impulse_joint_set: &mut ImpulseJointSet,
multibody_joint_set: &mut MultibodyJointSet,
) -> bool {
if let Some(id) = self.selected_id {
self.remove_shape(
id,
rigid_body_set,
collider_set,
island_manager,
impulse_joint_set,
multibody_joint_set,
)
} else {
false
}
}
pub fn find_random_position(
&self,
shape_type: ShapeType,
world_width: f32,
world_height: f32,
max_attempts: u32,
) -> Option<(f32, f32)> {
let mut rng = rand::thread_rng();
let ascii_art = get_ascii_art(shape_type);
let half_width = ascii_art.width() as f32 / 2.0;
let half_height = ascii_art.height() as f32 / 2.0;
let spawn_zone_bottom = world_height * 0.75;
let min_x = EDGE_MARGIN + half_width;
let max_x = world_width - EDGE_MARGIN - half_width;
let min_y = EDGE_MARGIN + half_height;
let max_y = spawn_zone_bottom - half_height;
if min_x >= max_x || min_y >= max_y {
return None;
}
for _ in 0..max_attempts {
let x = rng.gen_range(min_x..max_x);
let y = rng.gen_range(min_y..max_y);
let temp_shape = Shape::new(0, shape_type, x, y);
let overlaps = self
.shapes
.iter()
.any(|existing| shapes_overlap(&temp_shape, existing, MIN_SHAPE_SEPARATION));
if !overlaps {
return Some((x, y));
}
}
None
}
pub fn place_random_shape(
&mut self,
world_width: f32,
world_height: f32,
rigid_body_set: &mut RigidBodySet,
collider_set: &mut ColliderSet,
) -> Option<u32> {
let mut rng = rand::thread_rng();
let shape_types = ShapeType::all();
let shape_type = shape_types[rng.gen_range(0..shape_types.len())];
let position = self.find_random_position(shape_type, world_width, world_height, 100)?;
let id = self.add_shape(
shape_type,
position.0,
position.1,
rigid_body_set,
collider_set,
);
Some(id)
}
pub fn place_initial_shapes(
&mut self,
world_width: f32,
world_height: f32,
rigid_body_set: &mut RigidBodySet,
collider_set: &mut ColliderSet,
) -> usize {
let mut rng = rand::thread_rng();
let count = rng.gen_range(2..=4);
let mut placed = 0;
for _ in 0..count {
if self
.place_random_shape(world_width, world_height, rigid_body_set, collider_set)
.is_some()
{
placed += 1;
}
}
placed
}
pub fn get_shape(&self, id: u32) -> Option<&Shape> {
self.shapes.iter().find(|s| s.id() == id)
}
pub fn get_shape_mut(&mut self, id: u32) -> Option<&mut Shape> {
self.shapes.iter_mut().find(|s| s.id() == id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_manager() {
let manager = ShapeManager::new();
assert_eq!(manager.shape_count(), 0);
assert!(manager.selected_id().is_none());
}
#[test]
fn test_find_random_position() {
let manager = ShapeManager::new();
let pos = manager.find_random_position(ShapeType::Square, 100.0, 100.0, 10);
assert!(pos.is_some());
let (x, y) = pos.unwrap();
assert!(x > 0.0 && x < 100.0);
assert!(y > 0.0 && y < 75.0); }
#[test]
fn test_find_random_position_small_world() {
let manager = ShapeManager::new();
let pos = manager.find_random_position(ShapeType::Square, 5.0, 5.0, 10);
assert!(pos.is_none());
}
}