embassy_ssd1306_graphics/lib.rs
1#![no_std]
2#![forbid(unsafe_code)]
3//! # embassy-ssd1306-graphics
4//!
5//! Couche graphique 2D `no_std` pour écrans OLED SSD1306 (128×64),
6//! construite au-dessus de `embassy-ssd1306`.
7//!
8//! ## Rôle exact de ce crate
9//!
10//! Le driver `embassy-ssd1306` fournit déjà :
11//! - `draw_pixel()`, `draw_hline()`, `draw_vline()`
12//! - `draw_rect()`, `draw_filled_rect()`
13//! - `draw_char()`, `draw_str()`, `draw_i16()`
14//! - `draw_bitmap()`
15//! - `clear()`, `fill()`, `flush()`
16//!
17//! Ce crate **ne duplique rien**. Il ajoute uniquement les primitives
18//! que le driver ne propose pas :
19//!
20//! | Fonction | Algorithme |
21//! |-------------------|-------------------------|
22//! | [`line()`] | Bresenham integer-only |
23//! | [`circle`] | Midpoint integer-only |
24//! | [`fill_circle`] | Midpoint + hlines |
25//! | [`triangle`] | 3 appels à `line()` |
26//! | [`ellipse`] | Midpoint généralisé |
27//! | [`bezier_quad`] | De Casteljau integer-only |
28//! | [`fill_triangle`] | Scanline integer-only |
29//! ## Architecture
30//!
31//! ```text
32//! ┌──────────────────────────────────────┐
33//! │ Votre application │
34//! │ line() / circle() / triangle() … │
35//! │ oled.draw_str() / oled.draw_i16() │ ← driver direct pour le texte
36//! └──────────┬───────────────────────────┘
37//! │ &mut Graphics │ &mut Ssd1306
38//! ┌──────────▼───────────┐ │
39//! │ Graphics (ce crate) │ │
40//! │ clipping · pixel() │ │
41//! └──────────┬───────────┘ │
42//! └─────────────────────┘
43//! │ draw_pixel()
44//! ┌──────────────────▼───────────────────┐
45//! │ embassy-ssd1306 (driver) │
46//! │ framebuffer · I2C · flush() │
47//! └──────────────────────────────────────┘
48//! ```
49//!
50//! ## Patron de borrow
51//!
52//! `Graphics` tient un `&mut Ssd1306` pour toute sa durée de vie.
53//! Pour appeler `oled.flush()`, `oled.clear()` ou `oled.draw_str()`,
54//! `gfx` doit être sorti de portée au préalable.
55//!
56//! ```rust,no_run
57//! loop {
58//! oled.clear();
59//! {
60//! let mut gfx = Graphics::new(&mut oled);
61//! line(&mut gfx, 0, 0, 127, 63, true);
62//! circle(&mut gfx, 64, 32, 20, true);
63//! } // ← borrow libéré
64//! oled.draw_str(40, 3, b"RPi2350");
65//! oled.flush().await.unwrap();
66//! }
67//! ```
68
69use embassy_ssd1306::Ssd1306;
70use embedded_hal_async::i2c::I2c;
71
72// ─────────────────────────────────────────────────────────────────────────────
73// Contexte graphique
74// ─────────────────────────────────────────────────────────────────────────────
75
76/// Contexte graphique.
77///
78/// Wraps minimalement un `&mut Ssd1306<I>` pour :
79/// - centraliser le **clipping** des coordonnées
80/// - fournir un `pixel()` signé (`i32`) aux algorithmes Bresenham / midpoint
81///
82/// Le driver reste propriétaire du framebuffer et du bus I2C.
83pub struct Graphics<'a, I: I2c> {
84 display: &'a mut Ssd1306<I>,
85}
86
87impl<'a, I: I2c> Graphics<'a, I> {
88 /// Crée un contexte graphique pour un écran 128×64.
89 #[inline]
90 pub fn new(display: &'a mut Ssd1306<I>) -> Self {
91 Self { display }
92 }
93
94 /// Dessine un pixel avec clipping automatique.
95 ///
96 /// Les coordonnées négatives ou hors de `[0, 128[` × `[0, 64[`
97 /// sont silencieusement ignorées aucun panic, aucun wrapping.
98 ///
99 /// Le driver gère lui-même un second clipping sur `u8` ;
100 /// ce niveau-ci permet aux algorithmes de travailler en `i32`
101 /// sans conversions coûteuses.
102 #[inline(always)]
103 pub fn pixel(&mut self, x: i32, y: i32, on: bool) {
104 if x >= 0 && y >= 0 && x < 128 && y < 64 {
105 self.display.draw_pixel(x as u8, y as u8, on);
106 }
107 }
108}
109
110// ─────────────────────────────────────────────────────────────────────────────
111// Ligne Bresenham
112// ─────────────────────────────────────────────────────────────────────────────
113
114/// Trace une ligne entre `(x0, y0)` et `(x1, y1)`.
115///
116/// **Algorithme :** Bresenham integer-only.
117/// Zéro division flottante, zéro multiplication,safe sur tout MCU sans FPU.
118///
119/// # Exemple
120///
121/// ```rust,no_run
122/// line(&mut gfx, 0, 0, 127, 63, true); // diagonale complète
123/// line(&mut gfx, 0, 0, 127, 63, false); // efface la diagonale
124/// ```
125pub fn line<I: I2c>(
126 gfx: &mut Graphics<'_, I>,
127 mut x0: i32,
128 mut y0: i32,
129 x1: i32,
130 y1: i32,
131 on: bool,
132) {
133 let dx = (x1 - x0).abs();
134 let sx = if x0 < x1 { 1 } else { -1 };
135 let dy = -(y1 - y0).abs();
136 let sy = if y0 < y1 { 1 } else { -1 };
137 let mut err = dx + dy;
138
139 loop {
140 gfx.pixel(x0, y0, on);
141 if x0 == x1 && y0 == y1 {
142 break;
143 }
144 let e2 = 2 * err;
145 if e2 >= dy {
146 err += dy;
147 x0 += sx;
148 }
149 if e2 <= dx {
150 err += dx;
151 y0 += sy;
152 }
153 }
154}
155
156// ─────────────────────────────────────────────────────────────────────────────
157// Cercle midpoint
158// ─────────────────────────────────────────────────────────────────────────────
159
160/// Trace le **contour** d'un cercle.
161///
162/// **Algorithme :** midpoint circle integer-only.
163/// Exploite la symétrie 8-octants : chaque itération dessine 8 pixels
164/// symétriques, ce qui minimise le nombre d'appels à `pixel()`.
165///
166/// # Paramètres
167///
168/// - `(cx, cy)` : centre
169/// - `r` : rayon en pixels
170///
171/// # Exemple
172///
173/// ```rust,no_run
174/// circle(&mut gfx, 64, 32, 20, true);
175/// ```
176pub fn circle<I: I2c>(gfx: &mut Graphics<'_, I>, cx: i32, cy: i32, r: i32, on: bool) {
177 if r <= 0 {
178 gfx.pixel(cx, cy, on);
179 return;
180 }
181 let mut x = r;
182 let mut y = 0;
183 let mut err = 0;
184
185 while x >= y {
186 gfx.pixel(cx + x, cy + y, on);
187 gfx.pixel(cx + y, cy + x, on);
188 gfx.pixel(cx - y, cy + x, on);
189 gfx.pixel(cx - x, cy + y, on);
190 gfx.pixel(cx - x, cy - y, on);
191 gfx.pixel(cx - y, cy - x, on);
192 gfx.pixel(cx + y, cy - x, on);
193 gfx.pixel(cx + x, cy - y, on);
194
195 y += 1;
196 if err <= 0 {
197 err += 2 * y + 1;
198 } else {
199 x -= 1;
200 err += 2 * (y - x) + 1;
201 }
202 }
203}
204
205/// **Remplit** un cercle (disque plein).
206///
207/// Utilise le même algorithme midpoint, mais dessine des lignes
208/// horizontales entre les points symétriques à chaque rangée.
209/// Beaucoup plus rapide que d'appeler `circle()` en spirale.
210///
211/// # Exemple
212///
213/// ```rust,no_run
214/// fill_circle(&mut gfx, 64, 32, 15, true);
215/// ```
216pub fn fill_circle<I: I2c>(gfx: &mut Graphics<'_, I>, cx: i32, cy: i32, r: i32, on: bool) {
217 if r <= 0 {
218 gfx.pixel(cx, cy, on);
219 return;
220 }
221 let mut x = r;
222 let mut y = 0;
223 let mut err = 0;
224
225 while x >= y {
226 // Lignes horizontales symétriques (haut/bas, gauche/droite)
227 for px in (cx - x)..=(cx + x) {
228 gfx.pixel(px, cy + y, on);
229 gfx.pixel(px, cy - y, on);
230 }
231 for px in (cx - y)..=(cx + y) {
232 gfx.pixel(px, cy + x, on);
233 gfx.pixel(px, cy - x, on);
234 }
235
236 y += 1;
237 if err <= 0 {
238 err += 2 * y + 1;
239 } else {
240 x -= 1;
241 err += 2 * (y - x) + 1;
242 }
243 }
244}
245
246// ─────────────────────────────────────────────────────────────────────────────
247// Triangle
248// ─────────────────────────────────────────────────────────────────────────────
249
250/// Trace le **contour** d'un triangle défini par trois sommets.
251///
252/// Implémenté comme trois appels à [`line()`], aucune logique propre.
253///
254/// # Exemple
255///
256/// ```rust,no_run
257/// triangle(&mut gfx, 64, 4, 20, 59, 108, 59, true);
258/// ```
259#[inline]
260pub fn triangle<I: I2c>(
261 gfx: &mut Graphics<'_, I>,
262 x0: i32, y0: i32,
263 x1: i32, y1: i32,
264 x2: i32, y2: i32,
265 on: bool,
266) {
267 line(gfx, x0, y0, x1, y1, on);
268 line(gfx, x1, y1, x2, y2, on);
269 line(gfx, x2, y2, x0, y0, on);
270}
271
272
273
274
275// ─────────────────────────────────────────────────────────────────────────────
276// Ellipse midpoint généralisé
277// ─────────────────────────────────────────────────────────────────────────────
278
279/// Trace le **contour** d'une ellipse.
280///
281/// **Algorithme :** midpoint ellipse integer-only (Bresenham généralisé).
282/// Deux phases : région 1 (pente < -1) puis région 2 (pente > -1).
283///
284/// # Paramètres
285///
286/// - `(cx, cy)` : centre
287/// - `rx` : demi-axe horizontal
288/// - `ry` : demi-axe vertical
289///
290/// # Exemple
291///
292/// ```rust,no_run
293/// ellipse(&mut gfx, 64, 32, 40, 20, true); // ellipse large
294/// ellipse(&mut gfx, 64, 32, 10, 10, true); // cercle (rx == ry)
295/// ```
296pub fn ellipse<I: I2c>(gfx: &mut Graphics<'_, I>, cx: i32, cy: i32, rx: i32, ry: i32, on: bool) {
297 if rx <= 0 || ry <= 0 {
298 gfx.pixel(cx, cy, on);
299 return;
300 }
301
302 let rx2 = rx * rx;
303 let ry2 = ry * ry;
304
305 let mut x = 0i32;
306 let mut y = ry;
307
308 // Région 1
309 let mut d1 = ry2 - rx2 * ry + rx2 / 4;
310 let mut dx = 2 * ry2 * x;
311 let mut dy = 2 * rx2 * y;
312
313 while dx < dy {
314 gfx.pixel(cx + x, cy + y, on);
315 gfx.pixel(cx - x, cy + y, on);
316 gfx.pixel(cx + x, cy - y, on);
317 gfx.pixel(cx - x, cy - y, on);
318
319 x += 1;
320 dx += 2 * ry2;
321 if d1 < 0 {
322 d1 += dx + ry2;
323 } else {
324 y -= 1;
325 dy -= 2 * rx2;
326 d1 += dx - dy + ry2;
327 }
328 }
329
330 // Région 2
331 let mut d2 = ry2 * (x * x + x) + rx2 * (y * y - 2 * y + 1) - rx2 * ry2 + rx2;
332
333 while y >= 0 {
334 gfx.pixel(cx + x, cy + y, on);
335 gfx.pixel(cx - x, cy + y, on);
336 gfx.pixel(cx + x, cy - y, on);
337 gfx.pixel(cx - x, cy - y, on);
338
339 y -= 1;
340 dy -= 2 * rx2;
341 if d2 > 0 {
342 d2 += rx2 - dy;
343 } else {
344 x += 1;
345 dx += 2 * ry2;
346 d2 += dx - dy + rx2;
347 }
348 }
349}
350
351// ─────────────────────────────────────────────────────────────────────────────
352// Courbe de Bézier quadratique De Casteljau
353// ─────────────────────────────────────────────────────────────────────────────
354
355/// Trace une **courbe de Bézier quadratique** (3 points de contrôle).
356///
357/// **Algorithme :** De Casteljau integer-only avec subdivision fixe.
358/// `steps` contrôle la finesse du tracé (16–32 suffisent pour 128×64).
359///
360/// Les interpolations sont faites en entiers avec précision ×1024
361/// pour éviter tout flottant.
362///
363/// # Paramètres
364///
365/// - `(x0, y0)` : point de départ
366/// - `(x1, y1)` : point de contrôle
367/// - `(x2, y2)` : point d'arrivée
368/// - `steps` : nombre de segments (recommandé : 16 à 32)
369///
370/// # Exemple
371///
372/// ```rust,no_run
373/// bezier_quad(&mut gfx, 10, 50, 64, 5, 118, 50, 24, true); // arche
374/// ```
375pub fn bezier_quad<I: I2c>(
376 gfx: &mut Graphics<'_, I>,
377 x0: i32, y0: i32,
378 x1: i32, y1: i32,
379 x2: i32, y2: i32,
380 steps: i32,
381 on: bool,
382) {
383 if steps <= 0 {
384 return;
385 }
386
387 let mut px = x0;
388 let mut py = y0;
389
390 for i in 1..=steps {
391 // t = i / steps en virgule fixe ×1024
392 let t = (i * 1024) / steps; // t ∈ [0, 1024]
393 let t1 = 1024 - t; // 1-t
394
395 // B(t) = (1-t)²·P0 + 2(1-t)t·P1 + t²·P2 (tout ×1024²)
396 let nx = (t1 * t1 * x0 + 2 * t1 * t * x1 + t * t * x2) / (1024 * 1024);
397 let ny = (t1 * t1 * y0 + 2 * t1 * t * y1 + t * t * y2) / (1024 * 1024);
398
399 line(gfx, px, py, nx, ny, on);
400 px = nx;
401 py = ny;
402 }
403}
404
405// ─────────────────────────────────────────────────────────────────────────────
406// Triangle plein scanline
407// ─────────────────────────────────────────────────────────────────────────────
408
409/// **Remplit** un triangle défini par trois sommets.
410///
411/// **Algorithme :** scanline — tri des sommets par Y, puis
412/// interpolation linéaire integer-only des bords gauche/droit
413/// à chaque rangée horizontale.
414///
415/// # Exemple
416///
417/// ```rust,no_run
418/// fill_triangle(&mut gfx, 64, 4, 20, 59, 108, 59, true);
419/// ```
420pub fn fill_triangle<I: I2c>(
421 gfx: &mut Graphics<'_, I>,
422 x0: i32, mut y0: i32,
423 x1: i32, mut y1: i32,
424 x2: i32, mut y2: i32,
425 on: bool,
426) {
427 // Tri des sommets par Y croissant (bubble sort sur 3 éléments)
428 let (mut x0, mut x1, mut x2) = (x0, x1, x2);
429 if y0 > y1 { core::mem::swap(&mut y0, &mut y1); core::mem::swap(&mut x0, &mut x1); }
430 if y1 > y2 { core::mem::swap(&mut y1, &mut y2); core::mem::swap(&mut x1, &mut x2); }
431 if y0 > y1 { core::mem::swap(&mut y0, &mut y1); core::mem::swap(&mut x0, &mut x1); }
432
433 let total_h = y2 - y0;
434 if total_h == 0 {
435 // Triangle dégénéré tracer une seule ligne
436 let xmin = x0.min(x1).min(x2);
437 let xmax = x0.max(x1).max(x2);
438 for x in xmin..=xmax {
439 gfx.pixel(x, y0, on);
440 }
441 return;
442 }
443
444 let upper_h = y1 - y0;
445 let lower_h = y2 - y1;
446
447 // Moitié supérieure : y0 → y1
448 for y in y0..=y1 {
449 let dy = y - y0;
450 // Interpolation integer-only ×total_h pour éviter la division
451 let xa = x0 + (x2 - x0) * dy / total_h;
452 let xb = if upper_h == 0 {
453 x1
454 } else {
455 x0 + (x1 - x0) * dy / upper_h
456 };
457 let (xmin, xmax) = if xa < xb { (xa, xb) } else { (xb, xa) };
458 for x in xmin..=xmax {
459 gfx.pixel(x, y, on);
460 }
461 }
462
463 // Moitié inférieure : y1 → y2
464 for y in y1..=y2 {
465 let dy = y - y0;
466 let xa = x0 + (x2 - x0) * dy / total_h;
467 let xb = if lower_h == 0 {
468 x1
469 } else {
470 x1 + (x2 - x1) * (y - y1) / lower_h
471 };
472 let (xmin, xmax) = if xa < xb { (xa, xb) } else { (xb, xa) };
473 for x in xmin..=xmax {
474 gfx.pixel(x, y, on);
475 }
476 }
477}