Skip to main content

drew_physics/
lib.rs

1// Copyright (C) 2026 Jorge Andre Castro
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 2 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12
13#![no_std]
14#![deny(missing_docs)]
15#![forbid(unsafe_code)]
16
17//! # Moteur Physique 2D `no_std`
18//!
19//! Moteur de simulation physique 2D minimaliste, déterministe et conçu pour
20//! les environnements contraints ou embarqués sans allocation dynamique (`#![no_std]`).
21//!
22//! ## Caractéristiques
23//!
24//! - **Sans allocation (`no_std`)** : Utilise des tableaux à taille fixe génériques `[Option<Body>; N]`.
25//! - **Intégration d'Euler semi-implicite** : Garantit une bonne stabilité numérique pour la vélocité et la position.
26//! - **Gestion des frontières** : Détection et résolution automatique des collisions avec rebond (`restitution`).
27//! - **Viscosité et gravité** : Prise en charge des forces environnementales globales.
28//!
29//! ## Exemple d'utilisation
30//!
31//! ```ignore
32//! use drew_physics::{Body, Vec2, World, WorldSettings};
33//!
34//! // Configuration de l'environnement
35//! let settings = WorldSettings {
36//!     gravity: Vec2::new(0.0, 9.81),
37//!     viscosity: 0.1,
38//!     bounds_min: Vec2::ZERO,
39//!     bounds_max: Vec2::new(800.0, 600.0),
40//! };
41//!
42//! // Instanciation d'un monde capable de contenir jusqu'à 10 corps
43//! let mut world = World::<10>::new(settings);
44//!
45//! // Création d'un corps dynamique (masse = 2.0, rayon = 5.0)
46//! let body = Body::new(Vec2::new(100.0, 100.0), 2.0, 5.0);
47//! let body_idx = world.add_body(body).expect("Monde plein");
48//!
49//! // Simulation d'un pas de temps (dt = 16ms)
50//! world.step(0.016);
51//! ```
52
53use core::ops::{Add, AddAssign, Mul, Sub};
54
55/// Vecteur 2D à composantes en virgule flottante (`f32`).
56#[derive(Clone, Copy, Debug, Default, PartialEq)]
57pub struct Vec2 {
58    /// Composante sur l'axe horizontal X.
59    pub x: f32,
60    /// Composante sur l'axe vertical Y.
61    pub y: f32,
62}
63
64impl Vec2 {
65    /// Vecteur nul `(0.0, 0.0)`.
66    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
67
68    /// Crée un nouveau vecteur 2D avec les composantes `x` et `y`.
69    pub const fn new(x: f32, y: f32) -> Self {
70        Self { x, y }
71    }
72
73    /// Calcule la norme au carré du vecteur (x² + y²).
74    ///
75    /// Utile pour comparer des distances sans coût d'extraction de racine carrée (`sqrt`).
76    pub fn length_squared(&self) -> f32 {
77        self.x * self.x + self.y * self.y
78    }
79}
80
81impl Add for Vec2 {
82    type Output = Self;
83    fn add(self, rhs: Self) -> Self {
84        Self {
85            x: self.x + rhs.x,
86            y: self.y + rhs.y,
87        }
88    }
89}
90
91impl AddAssign for Vec2 {
92    fn add_assign(&mut self, rhs: Self) {
93        self.x += rhs.x;
94        self.y += rhs.y;
95    }
96}
97
98impl Sub for Vec2 {
99    type Output = Self;
100    fn sub(self, rhs: Self) -> Self {
101        Self {
102            x: self.x - rhs.x,
103            y: self.y - rhs.y,
104        }
105    }
106}
107
108impl Mul<f32> for Vec2 {
109    type Output = Self;
110    fn mul(self, rhs: f32) -> Self {
111        Self {
112            x: self.x * rhs,
113            y: self.y * rhs,
114        }
115    }
116}
117
118/// Description d'un corps physique particulaire dans la simulation.
119#[derive(Clone, Copy, Debug)]
120pub struct Body {
121    /// Position actuelle du corps dans l'espace 2D.
122    pub position: Vec2,
123    /// Vecteur vitesse linéaire.
124    pub velocity: Vec2,
125    /// Accélération accumulée pour le pas de temps courant.
126    pub acceleration: Vec2,
127    /// Inverse de la masse (1 / m). Une valeur de `0.0` indique une masse infinie (corps statique/immobile).
128    pub inv_mass: f32,
129    /// Coefficient de restitution / rebond (compris entre `0.0` pour un choc inélastique et `1.0` pour un choc parfait).
130    pub restitution: f32,
131    /// Rayon du corps (utilisé pour la détection des collisions avec les limites du monde).
132    pub radius: f32,
133}
134
135impl Body {
136    /// Crée un nouveau corps physique à la position donnée.
137    ///
138    /// Si `mass <= 0.0`, l'inverse de la masse (`inv_mass`) sera fixé à `0.0`,
139    /// rendant le corps statique (insensible à la gravité et aux forces).
140    pub fn new(position: Vec2, mass: f32, radius: f32) -> Self {
141        let inv_mass = if mass <= 0.0 { 0.0 } else { 1.0 / mass };
142        Self {
143            position,
144            velocity: Vec2::ZERO,
145            acceleration: Vec2::ZERO,
146            inv_mass,
147            restitution: 0.8,
148            radius,
149        }
150    }
151
152    /// Indique si le corps est statique (masse infinie / `inv_mass == 0.0`).
153    pub fn is_static(&self) -> bool {
154        self.inv_mass == 0.0
155    }
156
157    /// Applique une force extérieure au corps.
158    ///
159    /// Ne produit aucun effet si le corps est statique (`inv_mass == 0.0`).
160    pub fn apply_force(&mut self, force: Vec2) {
161        if self.inv_mass > 0.0 {
162            self.acceleration += force * self.inv_mass;
163        }
164    }
165}
166
167/// Paramètres globaux de l'environnement physique.
168#[derive(Clone, Copy, Debug)]
169pub struct WorldSettings {
170    /// Vecteur d'accélération de la pesanteur (ex: `Vec2::new(0.0, 9.81)`).
171    pub gravity: Vec2,
172    /// Coefficient de frottement/viscosité du milieu entraînant une traînée sur les corps en mouvement.
173    pub viscosity: f32,
174    /// Coin supérieur gauche (coordonnées minimales) de la boîte de confinement.
175    pub bounds_min: Vec2,
176    /// Coin inférieur droit (coordonnées maximales) de la boîte de confinement.
177    pub bounds_max: Vec2,
178}
179
180/// Monde physique gérant l'intégration et la simulation globale.
181///
182/// Stocke un ensemble de corps dans un tableau statique de taille `N`, évitant toute
183/// allocation sur le tas (`heap`).
184pub struct World<const N: usize> {
185    /// Emplacements réservés aux corps physiques du monde.
186    pub bodies: [Option<Body>; N],
187    /// Configuration globale du monde physique.
188    pub settings: WorldSettings,
189}
190
191impl<const N: usize> World<N> {
192    /// Crée un nouveau monde physique avec les paramètres spécifiés.
193    pub fn new(settings: WorldSettings) -> Self {
194        Self {
195            bodies: [None; N],
196            settings,
197        }
198    }
199
200    /// Renvoie un itérateur mutable sur la liste des corps du monde.
201    pub fn bodies_mut(&mut self) -> &mut [Option<Body>; N] {
202        &mut self.bodies
203    }
204
205    /// Ajoute un corps dans le premier emplacement disponible du monde.
206    ///
207    /// Renvoie `Some(index)` en cas de succès, ou `None` si la capacité maximale `N` est atteinte.
208    pub fn add_body(&mut self, body: Body) -> Option<usize> {
209        for (idx, slot) in self.bodies.iter_mut().enumerate() {
210            if slot.is_none() {
211                *slot = Some(body);
212                return Some(idx);
213            }
214        }
215        None
216    }
217
218    /// Fait avancer la simulation d'un intervalle de temps `dt` (en secondes).
219    ///
220    /// Applique la gravité, calcule l'intégration semi-implicite d'Euler, applique
221    /// la viscosité du milieu et résout les collisions avec les limites du monde.
222    pub fn step(&mut self, dt: f32) {
223        for slot in self.bodies.iter_mut() {
224            if let Some(body) = slot {
225                if body.is_static() {
226                    continue; // Corps statique
227                }
228
229                // 1. Accumulation initiale avec la gravité
230                body.acceleration += self.settings.gravity;
231
232                // 2. Intégration d'Euler semi-implicite
233                body.velocity += body.acceleration * dt;
234
235                // 3. Application de la viscosité du milieu
236                if self.settings.viscosity > 0.0 {
237                    let damping = (1.0 - self.settings.viscosity * dt).max(0.0);
238                    body.velocity = body.velocity * damping;
239                }
240
241                // 4. Mise à jour de la position
242                body.position += body.velocity * dt;
243
244                // 5. Remise à zéro de l'accélération pour la frame suivante
245                body.acceleration = Vec2::ZERO;
246
247                // 6. Gestion des collisions avec les bordures
248                Self::resolve_boundaries(body, &self.settings);
249            }
250        }
251    }
252
253    /// Détecte et résout les collisions d'un corps avec les bordures du monde.
254    fn resolve_boundaries(body: &mut Body, settings: &WorldSettings) {
255        let min_x = settings.bounds_min.x + body.radius;
256        let max_x = settings.bounds_max.x - body.radius;
257        let min_y = settings.bounds_min.y + body.radius;
258        let max_y = settings.bounds_max.y - body.radius;
259
260        if body.position.x < min_x {
261            body.position.x = min_x;
262            body.velocity.x = -body.velocity.x * body.restitution;
263        } else if body.position.x > max_x {
264            body.position.x = max_x;
265            body.velocity.x = -body.velocity.x * body.restitution;
266        }
267
268        if body.position.y < min_y {
269            body.position.y = min_y;
270            body.velocity.y = -body.velocity.y * body.restitution;
271        } else if body.position.y > max_y {
272            body.position.y = max_y;
273            body.velocity.y = -body.velocity.y * body.restitution;
274        }
275    }
276}