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}