diff --git a/matrice_3x3/README.md b/matrice_3x3/README.md
new file mode 100644
index 0000000..2ae0e0f
--- /dev/null
+++ b/matrice_3x3/README.md
@@ -0,0 +1,293 @@
+# Maquette 3 × 3 : matrice de LED RGB multiplexée
+
+Workshop « Vitrine pixels » : on fabrique un petit écran de **9 pixels** de
+couleur, soudé sur une plaque à pastilles et piloté par un Arduino.
+C'est la maquette d'apprentissage avant la grande matrice 7 × 39.
+
+
+
+> **Le principe à retenir** : on n'allume jamais les 9 LED en même temps.
+> On allume **une ligne à la fois**, très vite (environ 333 fois par seconde),
+> et l'œil voit une image fixe. C'est le **multiplexage**. Tout est expliqué
+> en détail dans les commentaires de [`matrice_3x3.ino`](matrice_3x3.ino).
+
+---
+
+## Sommaire
+
+1. [Le matériel](#1-le-matériel)
+2. [Reconnaître les composants](#2-reconnaître-les-composants)
+3. [Le schéma électrique](#3-le-schéma-électrique)
+4. [Le montage sur plaque à pastilles](#4-le-montage-sur-plaque-à-pastilles)
+5. [Souder, étape par étape](#5-souder-étape-par-étape)
+6. [Relier à l'Arduino](#6-relier-à-larduino)
+7. [Téléverser le programme et tester](#7-téléverser-le-programme-et-tester)
+8. [Ça ne marche pas ?](#8-ça-ne-marche-pas-)
+9. [Le fichier Fritzing](#9-le-fichier-fritzing)
+
+---
+
+## 1. Le matériel
+
+| Quoi | Combien | Remarque |
+|---|---|---|
+| Arduino Uno + câble USB | 1 | |
+| LED RGB 5 mm **à cathode commune** | 9 | 4 pattes ; vérifier « common cathode » à l'achat |
+| Transistor **BC337** (NPN) | 3 | ou 2N2222 / S8050 : **attention, pattes dans un autre ordre** (voir 2) |
+| Résistance **220 Ω** | 3 | pour le rouge |
+| Résistance **150 Ω** | 6 | pour le vert et le bleu |
+| Résistance **1 kΩ** | 3 | pour les bases des transistors |
+| Plaque à pastilles (pas 2,54 mm) | 1 | au moins **32 × 34 trous** |
+| Fil nu étamé (ou chutes de pattes de résistances) | ~1 m | pour les liaisons côté soudures |
+| Fil isolé fin (monobrin) | ~50 cm | pour les 3 lignes de cathodes |
+| Fils souples avec embout mâle (« Dupont ») | 13 | vers l'Arduino : 9 rouges/verts/bleus, 3 jaunes, 1 noir |
+| Fer à souder, étain, pince coupante, pince plate | | |
+
+---
+
+## 2. Reconnaître les composants
+
+### La LED RGB à cathode commune
+
+Elle contient 3 petites LED (rouge, verte, bleue) qui partagent la même
+patte « moins » : la **cathode commune**, qui est **la patte la plus longue**.
+
+```
+ ___
+ / \ Ordre des pattes le plus courant :
+ | |
+ |_____| R K G B
+ | | | | │ │ │ └ bleu
+ | | | | │ │ └──── vert
+ R K | | │ └──────── cathode (la plus longue)
+ G B └──────────── rouge (souvent côté du méplat)
+```
+
+> ⚠️ L'ordre peut changer selon le fabricant. **Testez une LED avant de
+> souder** : cathode au GND de l'Arduino, et chaque autre patte, l'une après
+> l'autre, au 5V **à travers une résistance de 220 Ω** (jamais sans résistance !).
+
+### Le transistor BC337
+
+C'est un **interrupteur électronique** : quand un petit courant entre par la
+**base (B)**, il laisse passer un gros courant entre le **collecteur (C)** et
+l'**émetteur (E)**. Ici, chaque transistor allume ou éteint une ligne de LED.
+
+Face plate vers vous, pattes vers le bas, de gauche à droite : **C – B – E**.
+
+> ⚠️ Le **2N2222** et le **S8050** ont l'ordre inverse : **E – B – C**.
+> Si vous les utilisez, tournez-les d'un demi-tour (face plate vers le haut
+> de la plaque).
+
+### Les résistances
+
+Elles freinent le courant pour protéger les LED et l'Arduino. On lit leur
+valeur avec les anneaux de couleur :
+
+| Valeur | Anneaux | Rôle |
+|---|---|---|
+| 220 Ω | rouge – rouge – marron | LED rouges |
+| 150 Ω | marron – vert – marron | LED vertes et bleues |
+| 1 kΩ | marron – noir – rouge | bases des transistors |
+
+Une résistance n'a pas de sens : on peut la souder dans les deux sens.
+
+---
+
+## 3. Le schéma électrique
+
+
+
+Comment le lire :
+
+- **À gauche, l'Arduino**. Ses broches **D2 à D10** envoient la couleur,
+ ses broches **D11 à D13** choisissent la ligne allumée.
+- **Les 9 lignes verticales** : chaque broche D2 à D10 passe par une
+ résistance, puis rejoint **les anodes d'une même couleur dans une même
+ colonne** de la matrice. Par exemple, D2 alimente le rouge des 3 LED de la
+ colonne de gauche.
+- **Les 3 lignes horizontales** relient **les cathodes d'une même ligne**
+ de LED, puis vont au **collecteur** d'un transistor.
+- **À droite, les transistors** : leur base est commandée par D11, D12 ou D13
+ (à travers 1 kΩ), et leur émetteur est relié au **GND** (la masse).
+
+Un pixel s'allume quand **sa ligne est active** (transistor passant) **et**
+que **sa colonne envoie de la couleur**. C'est le croisement des deux qui
+choisit le pixel.
+
+| Broche Arduino | Rôle |
+|---|---|
+| D2, D3, D4 | rouge, vert, bleu de la **colonne 0** (gauche) |
+| D5, D6, D7 | rouge, vert, bleu de la **colonne 1** (milieu) |
+| D8, D9, D10 | rouge, vert, bleu de la **colonne 2** (droite) |
+| D11, D12, D13 | **lignes** 0 (haut), 1, 2 (bas), par les transistors T0, T1, T2 |
+| GND | masse : émetteurs des 3 transistors |
+
+---
+
+## 4. Le montage sur plaque à pastilles
+
+Le plan en haut de cette page montre la plaque **des deux côtés** :
+
+- **À gauche, côté composants** : ce qu'on voit quand la plaque est posée
+ devant soi, composants vers le haut.
+- **À droite, côté soudures** : la plaque **retournée**. ⚠️ C'est une image
+ **en miroir** : ce qui était à gauche se retrouve à droite. Les numéros de
+ colonnes aident à s'y retrouver.
+
+Les trous sont repérés par **colonne** (de 1 à 32, de gauche à droite côté
+composants) et **ligne** (de 1 à 34, de haut en bas).
+
+### L'idée du câblage
+
+- **Les colonnes de couleur** (anodes) sont des **fils nus verticaux**, côté
+ soudures. Comme les 3 LED d'une colonne sont alignées, leurs pattes R
+ tombent dans la même colonne de trous : un fil nu droit les relie toutes,
+ puis rejoint la résistance.
+- **Les lignes de cathodes** croisent les colonnes : on ne peut pas les faire
+ en fil nu du même côté (ça ferait un court-circuit !). On les fait donc en
+ **fil isolé, côté composants**, qui passe **par-dessus**.
+- Chaque fil isolé arrive dans **le trou juste à côté** de la patte à
+ relier ; on fait la liaison côté soudures avec une goutte d'étain ou un
+ bout de fil nu.
+
+### Où va chaque composant
+
+| Composant | Trous (colonne, ligne) |
+|---|---|
+| Résistances 220 Ω (rouge) | col. 3, 9, 15 : lignes 3 → 7 |
+| Résistances 150 Ω (vert) | col. 5, 11, 17 : lignes 3 → 7 |
+| Résistances 150 Ω (bleu) | col. 6, 12, 18 : lignes 8 → 12 |
+| LED de la ligne 0 (haut) | ligne 19 ; colonnes 3-6, 9-12, 15-18 (pattes R K G B) |
+| LED de la ligne 1 | ligne 25 ; mêmes colonnes |
+| LED de la ligne 2 (bas) | ligne 31 ; mêmes colonnes |
+| Transistors T0, T1, T2 | lignes 20, 26, 32 ; colonnes 22 (C), 23 (B), 24 (E) |
+| Résistances 1 kΩ (bases) | lignes 21, 27, 33 ; colonnes 23 → 27 |
+
+> **Pourquoi des LED espacées de 6 trous (15 mm) ?** Pour laisser de la place
+> aux fils et aux doigts. La matrice finale aura un pas de 10 mm. Pour tester
+> la diffusion de la lumière à 10 mm, on peut refaire une version au pas de
+> 4 trous (10,16 mm), mais le câblage est beaucoup plus serré.
+
+---
+
+## 5. Souder, étape par étape
+
+**On soude des composants les plus bas aux plus hauts** : la plaque reste
+stable quand on la retourne.
+
+1. **Les fils nus** (côté soudures) : les 9 colonnes de couleur, le bus de
+ masse (colonne 31) et les petits ponts. Suivez le plan de droite.
+2. **Les 12 résistances**, couchées à plat contre la plaque (le long des
+ colonnes pour les 9 résistances de couleur, le long des lignes pour les 1 kΩ).
+ Vérifiez les couleurs : 220 Ω pour le rouge, 150 Ω pour le vert et le bleu,
+ 1 kΩ pour les bases.
+3. **Les 3 transistors**, face plate vers le bas de la plaque.
+ Ne chauffez pas plus de 3 secondes par patte.
+4. **Les 9 LED** : patte la plus longue (K) en 2e position.
+ **Testez chaque LED avant** (voir partie 2).
+5. **Les 3 fils isolés** des cathodes (côté composants).
+6. **Les 13 fils vers l'Arduino** (voir partie 6).
+
+Conseils de soudure :
+
+- Le fer chauffe **la pastille et la patte en même temps**, puis on apporte
+ l'étain. Une bonne soudure est brillante et en forme de petit volcan.
+- Après chaque étape, **vérifiez qu'aucune goutte d'étain ne relie deux
+ pastilles voisines** par erreur, surtout entre les colonnes de couleur
+ voisines (G et B ne sont qu'à un trou d'écart !).
+- Coupez les pattes qui dépassent **après** avoir soudé. Gardez les chutes :
+ elles font d'excellents fils nus.
+
+---
+
+## 6. Relier à l'Arduino
+
+| Fil | Trou sur la plaque (col., ligne) | Broche Arduino |
+|---|---|---|
+| 🔴 rouge colonne 0 | 3, 2 | D2 |
+| 🟢 vert colonne 0 | 5, 2 | D3 |
+| 🔵 bleu colonne 0 | 6, 2 | D4 |
+| 🔴 rouge colonne 1 | 9, 2 | D5 |
+| 🟢 vert colonne 1 | 11, 2 | D6 |
+| 🔵 bleu colonne 1 | 12, 2 | D7 |
+| 🔴 rouge colonne 2 | 15, 2 | D8 |
+| 🟢 vert colonne 2 | 17, 2 | D9 |
+| 🔵 bleu colonne 2 | 18, 2 | D10 |
+| 🟡 ligne 0 | 28, 21 | D11 |
+| 🟡 ligne 1 | 29, 27 | D12 |
+| 🟡 ligne 2 | 30, 33 | D13 |
+| ⚫ masse | 31, 18 | GND (à côté de D13) |
+
+
+
+> Pas besoin d'alimentation externe : 9 LED à 15 mA, une seule ligne à la fois,
+> c'est au maximum ~135 mA. Le port USB suffit.
+
+---
+
+## 7. Téléverser le programme et tester
+
+1. Installez l'**IDE Arduino** et la bibliothèque **TimerOne**
+ (Outils → Gérer les bibliothèques… → « TimerOne » → Installer).
+2. Ouvrez [`matrice_3x3.ino`](matrice_3x3.ino).
+3. Outils → Type de carte → **Arduino Uno** ; Outils → Port → le bon port.
+4. **Téléverser**.
+
+La première animation, `animTestPixels`, allume **chaque pixel l'un après
+l'autre**, en rouge, puis en vert, puis en bleu, de gauche à droite et de haut
+en bas. C'est le test de câblage : notez tout pixel qui ne s'allume pas, ou
+pas au bon endroit, ou pas dans la bonne couleur.
+
+Ensuite, les animations s'enchaînent : aplats de couleur, chenille, damier,
+balayages, scintillement. Pour créer les vôtres, lisez les parties 5 et 6 du
+programme.
+
+---
+
+## 8. Ça ne marche pas ?
+
+| Symptôme | Cause probable | Solution |
+|---|---|---|
+| **Rien ne s'allume** | Fil de masse (GND) absent | Vérifier le fil noir et le bus de masse (col. 31) |
+| | Transistors à l'envers | Vérifier C-B-E (BC337) ou E-B-C (2N2222) |
+| **Une ligne entière reste éteinte** | Transistor mal soudé ou à l'envers | Vérifier T0, T1 ou T2 et sa résistance de base |
+| | Fil isolé de cathode mal soudé | Vérifier le pont entre la patte K et le fil |
+| **Une couleur manque sur toute une colonne** | Résistance ou fil nu de colonne mal soudé | Suivre la colonne côté soudures |
+| | Mauvais fil sur l'Arduino | Comparer avec le tableau de la partie 6 |
+| **Un seul pixel ne marche pas** | LED mal soudée, ou grillée | Ressouder, ou tester la LED seule |
+| **Couleurs mélangées** (rouge à la place du bleu…) | Ordre des pattes de la LED différent | Tester la LED (partie 2) ; corriger dans le code (`pinsCouleur`) |
+| **Deux couleurs s'allument ensemble** | Pont d'étain entre deux colonnes voisines | Chauffer et retirer l'étain en trop (tresse à dessouder) |
+| **Légers reflets sur des pixels éteints** (« fantômes ») | Normal si très faible | Le code éteint déjà les lignes avant de changer les couleurs |
+| **L'image clignote** | `DUREE_LIGNE_US` trop grand | Revenir à 1000 dans le code |
+| **La petite LED « L » de l'Arduino scintille** | Normal : elle est sur la broche 13 (ligne 2) | Rien à faire |
+
+---
+
+## 9. Le fichier Fritzing
+
+[`matrice_3x3.fzz`](matrice_3x3.fzz) s'ouvre avec
+[Fritzing](https://fritzing.org) (version 1.0 ou plus récente). Il contient :
+
+- la vue **Schéma** : le même circuit que la partie 3, avec les symboles
+ de Fritzing ([aperçu](schemas/schema_fritzing.png)) ;
+- la vue **Platine d'essai** : la plaque à pastilles reliée à l'Arduino.
+ Les fils gris sont les liaisons en fil nu (côté soudures), les fils noirs
+ sur la plaque sont les fils isolés des cathodes.
+
+La vue **Circuit imprimé** n'est pas utilisée (pièces non placées).
+
+Les scripts du dossier [`outils/`](outils/) régénèrent le fichier Fritzing,
+le plan d'implantation et le schéma électrique si on modifie le montage :
+
+```bash
+python3 outils/gen_fzz.py matrice_3x3.fzz
+```
+
+```bash
+python3 outils/render_plaque.py schemas/implantation_plaque.svg
+```
+
+```bash
+python3 outils/schema_electrique.py schemas/schema_electrique.svg
+```
diff --git a/matrice_3x3/matrice_3x3.fzz b/matrice_3x3/matrice_3x3.fzz
new file mode 100644
index 0000000..b06c908
Binary files /dev/null and b/matrice_3x3/matrice_3x3.fzz differ
diff --git a/matrice_3x3/outils/gen_fzz.py b/matrice_3x3/outils/gen_fzz.py
new file mode 100644
index 0000000..ded5744
--- /dev/null
+++ b/matrice_3x3/outils/gen_fzz.py
@@ -0,0 +1,489 @@
+#!/usr/bin/env python3
+"""Génère matrice_3x3.fzz : maquette 3x3 sur plaque à pastilles (vue platine)
++ schéma électrique (vue schéma). Coordonnées en px Fritzing (90 dpi)."""
+import math, sys, zipfile, os
+from xml.sax.saxutils import escape
+
+PARTS = "/usr/share/fritzing/fritzing-parts/core/"
+P = 9.0 # pas 0,1 pouce
+
+# ---------------------------------------------------------------- modèle
+instances = [] # dict: id, module, path, title, props, views{view:{...}}
+idx = [1000]
+def nid():
+ idx[0] += 1
+ return idx[0]
+
+conns = {} # (inst_id, view, connector) -> list of (other_id, other_connector, other_layer)
+def link(view, a, ca, la, b, cb, lb):
+ conns.setdefault((a, view, ca), []).append((b, cb, lb))
+ conns.setdefault((b, view, cb), []).append((a, ca, la))
+
+# ---------------------------------------------------------------- géométries des pièces
+LED = dict(module="d4d5af9700923b8a114f57961f29a8a0ColorLEDModuleID",
+ path=PARTS + "led-rgb-4pin-cathode_v5.fzp",
+ legs={"connector0": (1.35, 40.6875), "connector1": (10.35, 40.6875),
+ "connector2": (19.35, 40.6875), "connector3": (28.35, 40.6875)},
+ size=(0.33 * 90, 0.4521 * 90))
+NPN = dict(module="2244EBEC10e", path=PARTS + "transistor_signal_NPN_TO92_CBE.fzp",
+ # connector2 = C (gauche), connector1 = B, connector0 = E (droite)
+ legs={"connector2": (1.35, 30.101), "connector1": (10.326, 30.101),
+ "connector0": (19.309, 30.101)},
+ size=(0.22954 * 90, 0.3344583 * 90))
+RES = dict(module="ResistorModuleID", path=PARTS + "resistor.fzp",
+ legs={"connector0": (2.619, 4.5405), "connector1": (36.0063, 4.5405)},
+ size=(0.42917 * 90, 0.0971 * 90))
+ARDUINO = dict(module="arduino_Uno_Rev3(fix)", path=PARTS + "arduino_Uno_Rev3(fix).fzp",
+ size=(2.94961 * 90, 2.1 * 90))
+ARD_PIN = { # broche -> (connector id, x, y) dans le repère de la carte (non tournée)
+ "D2": ("connector63", 229.47, 9.0), "D3": ("connector64", 220.46, 9.0),
+ "D4": ("connector65", 211.46, 9.0), "D5": ("connector66", 202.46, 9.0),
+ "D6": ("connector67", 193.46, 9.0), "D7": ("connector68", 184.47, 9.0),
+ "D8": ("connector51", 170.06, 9.0), "D9": ("connector52", 161.06, 9.0),
+ "D10": ("connector53", 152.06, 9.0), "D11": ("connector54", 143.06, 9.0),
+ "D12": ("connector55", 134.06, 9.0), "D13": ("connector56", 125.06, 9.0),
+ "GND": ("connector57", 116.06, 9.0),
+}
+
+def rot_matrix(deg, w, h):
+ """Rotation autour du centre (w/2, h/2), au format QTransform de Fritzing."""
+ cx, cy = w / 2, h / 2
+ a = math.radians(deg)
+ c, s = round(math.cos(a), 9), round(math.sin(a), 9)
+ m11, m12, m21, m22 = c, s, -s, c
+ m31 = cx - (m11 * cx + m21 * cy)
+ m32 = cy - (m12 * cx + m22 * cy)
+ return (m11, m12, m21, m22, m31, m32)
+
+def apply(m, x, y):
+ m11, m12, m21, m22, m31, m32 = m
+ return (m11 * x + m21 * y + m31, m12 * x + m22 * y + m32)
+
+def inv_apply(m, u, v):
+ m11, m12, m21, m22, m31, m32 = m
+ u, v = u - m31, v - m32
+ det = m11 * m22 - m12 * m21
+ return ((m22 * u - m21 * v) / det, (-m12 * u + m11 * v) / det)
+
+# ---------------------------------------------------------------- vue platine (plaque à pastilles)
+COLS, ROWS = 32, 34
+BOARD = dict(id=nid(), module=f"{COLS}.{ROWS}PerfboardModuleID",
+ path=":/resources/parts/core/perfboard.fzp", title="Plaque à pastilles",
+ props={"size": f"{COLS}.{ROWS}"}, views={})
+BOARD["views"]["breadboardView"] = dict(layer="breadboardbreadboard", x=0, y=0, z=1.5)
+instances.append(BOARD)
+
+def hole(c, r):
+ return (9 + P * c, 9 + P * r)
+
+def hole_id(c, r):
+ return f"connector{r * 1000 + c}"
+
+def add_part(spec, title, bb_pos, bb_rot=0, props=None, sch=None, legs_to=None):
+ """legs_to : {connector: (col, row)} trous de la plaque où arrivent les pattes."""
+ inst = dict(id=nid(), module=spec["module"], path=spec["path"], title=title,
+ props=props or {}, views={}, legs={})
+ w, h = spec["size"]
+ m = rot_matrix(bb_rot, w, h) if bb_rot else None
+ inst["views"]["breadboardView"] = dict(layer="breadboard", x=bb_pos[0], y=bb_pos[1],
+ z=2.5, m=m)
+ if legs_to:
+ for con, (c, r) in legs_to.items():
+ sx, sy = spec["legs"][con]
+ hx, hy = hole(c, r)
+ u, v = hx - bb_pos[0], hy - bb_pos[1]
+ lx, ly = inv_apply(m, u, v) if m else (u, v)
+ inst["legs"][con] = ((sx, sy), (lx - sx, ly - sy))
+ link("breadboardView", inst["id"], con, "breadboard", BOARD["id"],
+ hole_id(c, r), "breadboardbreadboard")
+ inst["sch"] = sch
+ inst["holes"] = legs_to or {}
+ instances.append(inst)
+ return inst
+
+def place_on_holes(spec, title, holes, rot=0, lift=1.0, props=None):
+ """Place la pièce pour que ses pattes soient au-dessus des trous, puis plie les
+ pattes vers les trous. lift = longueur de patte visible, en pas de 0,1 po."""
+ cons = list(holes)
+ # centre des trous visés, centre des départs de pattes
+ hx = sum(hole(*holes[c])[0] for c in cons) / len(cons)
+ hy = sum(hole(*holes[c])[1] for c in cons) / len(cons)
+ sx = sum(spec["legs"][c][0] for c in cons) / len(cons)
+ sy = sum(spec["legs"][c][1] for c in cons) / len(cons)
+ w, h = spec["size"]
+ m = rot_matrix(rot, w, h) if rot else None
+ tx, ty = apply(m, sx, sy) if m else (sx, sy)
+ if rot == 0:
+ pos = (hx - tx, hy - ty - lift * P)
+ else:
+ pos = (hx - tx, hy - ty)
+ return add_part(spec, title, pos, rot, props, legs_to=holes)
+
+wires = []
+board_wires = [] # (trou a, trou b, couleur) pour le plan d'implantation
+def bb_wire(a, b, color, title=None):
+ """Fil entre deux trous de la plaque (a, b = (col, row))."""
+ (x1, y1), (x2, y2) = hole(*a), hole(*b)
+ board_wires.append((a, b, color))
+ w = dict(id=nid(), title=title or f"Fil{len(wires) + 1}", view="breadboardView",
+ x=x1, y=y1, dx=x2 - x1, dy=y2 - y1, color=color, flags=64,
+ layer="breadboardWire", z=(1.8 if color == NU else 3.5))
+ for k, (c, r) in enumerate((a, b)):
+ link("breadboardView", w["id"], f"connector{k}", "breadboardWire",
+ BOARD["id"], hole_id(c, r), "breadboardbreadboard")
+ wires.append(w)
+ return w
+
+ARD_POS = (40.0, -280.0)
+ARD_M = rot_matrix(180, *ARDUINO["size"])
+ARD = dict(id=nid(), module=ARDUINO["module"], path=ARDUINO["path"], title="Arduino Uno",
+ props={}, views={}, legs={})
+ARD["views"]["breadboardView"] = dict(layer="breadboardbreadboard", x=ARD_POS[0],
+ y=ARD_POS[1], z=1.6, m=ARD_M)
+instances.append(ARD)
+
+def ard_pin_xy(name):
+ _, px, py = ARD_PIN[name]
+ u, v = apply(ARD_M, px, py)
+ return (ARD_POS[0] + u, ARD_POS[1] + v)
+
+def ard_wire(pin, hole_cr, color):
+ x1, y1 = ard_pin_xy(pin)
+ x2, y2 = hole(*hole_cr)
+ w = dict(id=nid(), title=f"Fil {pin}", view="breadboardView", x=x1, y=y1,
+ dx=x2 - x1, dy=y2 - y1, color=color, flags=64, layer="breadboardWire")
+ link("breadboardView", w["id"], "connector0", "breadboardWire", ARD["id"],
+ ARD_PIN[pin][0], "breadboardbreadboard")
+ link("breadboardView", w["id"], "connector1", "breadboardWire", BOARD["id"],
+ hole_id(*hole_cr), "breadboardbreadboard")
+ wires.append(w)
+
+# couleurs de fils Fritzing
+NU = "#a8a8a8" # fil nu étamé, côté soudure
+ROUGE, VERT, BLEU = "#cc1414", "#47cc79", "#418dd9"
+NOIR, JAUNE, ORANGE = "#404040", "#ffe24d", "#ff7033"
+
+# --- implantation -------------------------------------------------------------
+PAS = 6 # pas entre LED, en trous (6 × 2,54 = 15,24 mm)
+X0, Y_LED = 2, (18, 24, 30) # LED (ligne r, colonne c) : pattes R K G B
+COUL = {"R": 0, "G": 2, "B": 3} # décalage de la patte dans la LED
+PIN_ANODE = {(c, k): f"D{2 + 3 * c + i}" for c in range(3) for i, k in enumerate("RGB")}
+RES_VAL = {"R": "220Ω", "G": "150Ω", "B": "150Ω"}
+COUL_FIL = {"R": ROUGE, "G": VERT, "B": BLEU}
+ard_jobs = [] # (broche, trou, couleur) routés à la fin
+
+leds = {}
+for r in range(3):
+ for c in range(3):
+ x = X0 + PAS * c
+ leds[(r, c)] = place_on_holes(
+ LED, f"LED L{r}C{c}",
+ {"connector0": (x, Y_LED[r]), "connector1": (x + 1, Y_LED[r]),
+ "connector2": (x + 2, Y_LED[r]), "connector3": (x + 3, Y_LED[r])},
+ lift=0.6)
+
+res_anode = {}
+for c in range(3):
+ for k, off in COUL.items():
+ col = X0 + PAS * c + off
+ top, bot = (2, 6) if k in "RG" else (7, 11)
+ res_anode[(c, k)] = place_on_holes(
+ RES, f"R {k}{c}", {"connector0": (col, top), "connector1": (col, bot)},
+ rot=90, props={"resistance": RES_VAL[k]})
+ bb_wire((col, 1), (col, top), NU)
+ ard_jobs.append((PIN_ANODE[(c, k)], (col, 1), COUL_FIL[k]))
+ bb_wire((col, bot), (col, Y_LED[0]), NU)
+ bb_wire((col, Y_LED[0]), (col, Y_LED[1]), NU)
+ bb_wire((col, Y_LED[1]), (col, Y_LED[2]), NU)
+
+XC, XB, XE = 21, 22, 23 # pattes C, B, E
+X_GND = 30
+trans, res_base = {}, {}
+for r in range(3):
+ y = Y_LED[r] + 1
+ for c in range(3):
+ xk = X0 + PAS * c + 1
+ bb_wire((xk, Y_LED[r]), (xk, y), NU)
+ bb_wire((X0 + 1, y), (X0 + 1 + PAS, y), NOIR)
+ bb_wire((X0 + 1 + PAS, y), (X0 + 1 + 2 * PAS, y), NOIR)
+ bb_wire((X0 + 1 + 2 * PAS, y), (XC - 1, y), NOIR)
+ bb_wire((XC - 1, y), (XC, y), NU)
+ trans[r] = place_on_holes(NPN, f"T{r} (BC337)",
+ {"connector2": (XC, y), "connector1": (XB, y),
+ "connector0": (XE, y)}, lift=0.6)
+ res_base[r] = place_on_holes(RES, f"Rb{r}",
+ {"connector0": (XB, y + 1), "connector1": (XB + 4, y + 1)},
+ lift=0.0, props={"resistance": "1kΩ"})
+ bb_wire((XB, y), (XB, y + 1), NU)
+ xw = XB + 5 + r
+ bb_wire((XB + 4, y + 1), (xw, y + 1), NU)
+ ard_jobs.append((f"D{11 + r}", (xw, y + 1), JAUNE))
+ bb_wire((XE, y), (X_GND, y), NU)
+yg = Y_LED[0] - 1
+bb_wire((X_GND, yg), (X_GND, Y_LED[0] + 1), NU)
+bb_wire((X_GND, Y_LED[0] + 1), (X_GND, Y_LED[1] + 1), NU)
+bb_wire((X_GND, Y_LED[1] + 1), (X_GND, Y_LED[2] + 1), NU)
+ard_jobs.append(("GND", (X_GND, yg), NOIR))
+
+# routage des fils Arduino : descente, palier horizontal, descente (fils coudés)
+def chain(points, color, pin, hole_cr):
+ ids = []
+ for i in range(len(points) - 1):
+ (x1, y1), (x2, y2) = points[i], points[i + 1]
+ w = dict(id=nid(), title=f"Fil {pin}", view="breadboardView", x=x1, y=y1,
+ dx=x2 - x1, dy=y2 - y1, color=color, flags=64, layer="breadboardWire")
+ wires.append(w)
+ ids.append(w["id"])
+ link("breadboardView", ids[0], "connector0", "breadboardWire", ARD["id"],
+ ARD_PIN[pin][0], "breadboardbreadboard")
+ link("breadboardView", ids[-1], "connector1", "breadboardWire", BOARD["id"],
+ hole_id(*hole_cr), "breadboardbreadboard")
+ for a_, b_ in zip(ids, ids[1:]):
+ link("breadboardView", a_, "connector1", "breadboardWire", b_, "connector0",
+ "breadboardWire")
+
+header_y = ard_pin_xy("D2")[1]
+jobs = [(pin, h, col, ard_pin_xy(pin)[0], hole(*h)[0]) for pin, h, col in ard_jobs]
+droite = sorted([j for j in jobs if j[4] > j[3] + 0.5], key=lambda j: j[3])
+gauche = sorted([j for j in jobs if j[4] < j[3] - 0.5], key=lambda j: -j[3])
+niveau = {}
+for groupe in (droite, gauche):
+ n = len(groupe)
+ for i, j in enumerate(groupe): # le plus à l'extérieur descend le plus bas
+ niveau[j[0]] = n - i
+for pin, h, col, xp, xh in jobs:
+ yp = header_y
+ if pin in niveau:
+ yl = header_y + 18 + 9 * niveau[pin]
+ chain([(xp, yp), (xp, yl), (xh, yl), hole(*h)], col, pin, h)
+ else:
+ chain([(xp, yp), hole(*h)], col, pin, h)
+
+# ---------------------------------------------------------------- vue schéma
+K = 90 / 96 # px Inkscape -> px Fritzing (SVG en pouces)
+SCH = { # terminaux dans le repère du symbole (px Fritzing)
+ "LED": {"connector0": (5.126, 35.457), "connector2": (23.172, 35.457),
+ "connector3": (41.297, 35.457), "connector1": (23.225, 1.5)},
+ "NPN": {"connector1": (0.4375, 18.4375), "connector2": (18.4375, 0.4375),
+ "connector0": (18.4375, 36.4375)},
+ "RES": {"connector0": (0.4375, 3.87), "connector1": (36.4375, 3.87)},
+ "RES_SIZE": (0.409722 * 90, 0.086 * 90),
+}
+def ard_sch(name):
+ con = ARD_PIN[name][0]
+ if name == "GND":
+ return (72.0, 207.0)
+ n = {"D2": 0, "D3": 1, "D4": 2, "D5": 3, "D6": 4, "D7": 5, "D8": 6, "D9": 7,
+ "D10": 8, "D11": 9, "D12": 10, "D13": 11}[name]
+ return (135.0, 54.0 + 9 * n)
+
+def set_sch(inst, pos, rot=0, size=None):
+ m = rot_matrix(rot, *size) if rot else None
+ inst["views"]["schematicView"] = dict(layer="schematic", x=pos[0], y=pos[1], z=2.5, m=m)
+ inst["sch_pos"], inst["sch_m"] = pos, m
+
+def place_sch(inst, table, con, target, rot=0, size=None):
+ """Place le symbole pour que le terminal `con` tombe sur `target`."""
+ m = rot_matrix(rot, *size) if rot else None
+ lx, ly = SCH[table][con]
+ if m:
+ lx, ly = apply(m, lx, ly)
+ set_sch(inst, (target[0] - lx, target[1] - ly), rot, size)
+
+def term(inst, table, con):
+ lx, ly = SCH[table][con]
+ if inst.get("sch_m"):
+ lx, ly = apply(inst["sch_m"], lx, ly)
+ return (inst["sch_pos"][0] + lx, inst["sch_pos"][1] + ly)
+
+ends = {} # point arrondi -> [(wire_id, connector)]
+def sch_poly(points, start=None, end=None):
+ ids = []
+ for (x1, y1), (x2, y2) in zip(points, points[1:]):
+ w = dict(id=nid(), title="Fil", view="schematicView", x=x1, y=y1, dx=x2 - x1,
+ dy=y2 - y1, color="#404040", flags=128, layer="schematicTrace",
+ mils="9.7222", z=5.5)
+ wires.append(w)
+ ids.append(w["id"])
+ for a_, b_ in zip(ids, ids[1:]):
+ link("schematicView", a_, "connector1", "schematicTrace", b_, "connector0",
+ "schematicTrace")
+ if start:
+ link("schematicView", ids[0], "connector0", "schematicTrace", start[0]["id"],
+ start[1], "schematic")
+ else:
+ ends.setdefault((round(points[0][0], 2), round(points[0][1], 2)), []).append(
+ (ids[0], "connector0"))
+ if end:
+ link("schematicView", ids[-1], "connector1", "schematicTrace", end[0]["id"],
+ end[1], "schematic")
+ else:
+ ends.setdefault((round(points[-1][0], 2), round(points[-1][1], 2)), []).append(
+ (ids[-1], "connector1"))
+
+A_POS = (0.0, 450.0)
+ARD["views"]["schematicView"] = dict(layer="schematic", x=A_POS[0], y=A_POS[1], z=2.5)
+def apin(name):
+ x, y = ard_sch(name)
+ return (A_POS[0] + x, A_POS[1] + y)
+ARDP = lambda name: (ARD, ARD_PIN[name][0])
+
+MX, MY, PASS = 225.0, 60.0, 90.0
+BUS_OFF = {"R": -27.0, "G": -18.0, "B": -9.0}
+PROF = {"R": 9.0, "G": 18.0, "B": 27.0}
+LEDCON = {"R": "connector0", "G": "connector2", "B": "connector3"}
+RES_TOP = {"R": 315.0, "G": 360.0, "B": 405.0}
+for (r, c), led in leds.items():
+ set_sch(led, (MX + PASS * c, MY + PASS * r))
+for c in range(3):
+ for k in "RGB":
+ bx = MX + PASS * c + BUS_OFF[k]
+ res = res_anode[(c, k)]
+ # patte 1 (connector0) côté Arduino, en bas ; patte 2 côté LED, en haut
+ place_sch(res, "RES", "connector1", (bx, RES_TOP[k]), rot=270, size=SCH["RES_SIZE"])
+ top, bot = term(res, "RES", "connector1"), term(res, "RES", "connector0")
+ pin = PIN_ANODE[(c, k)]
+ px, py = apin(pin)
+ sch_poly([(px, py), (bx, py), bot], start=ARDP(pin), end=(res, "connector0"))
+ # bus : résistance -> jonctions des lignes 2, 1, 0
+ pts = [top] + [(bx, MY + PASS * r + 35.457 + PROF[k]) for r in (2, 1, 0)]
+ for i, (a_, b_) in enumerate(zip(pts, pts[1:])):
+ sch_poly([a_, b_], start=(res, "connector1") if i == 0 else None)
+ for r in range(3):
+ led = leds[(r, c)]
+ tx, ty = term(led, "LED", LEDCON[k])
+ jy = MY + PASS * r + 35.457 + PROF[k]
+ sch_poly([(tx, ty), (tx, jy), (bx, jy)], start=(led, LEDCON[k]))
+
+XT = 545.0
+XV = {0: 462.0, 1: 471.0, 2: 480.0}
+X_GND_S, Y_GND_S = XT + 45.0, 690.0
+for r in range(3):
+ yrl = MY + PASS * r + 1.5 - 9.0
+ ks = [term(leds[(r, c)], "LED", "connector1") for c in range(3)]
+ for c in range(3):
+ sch_poly([ks[c], (ks[c][0], yrl)], start=(leds[(r, c)], "connector1"))
+ t = trans[r]
+ set_sch(t, (XT, yrl + 18.0 - 0.4375))
+ cpt, bpt, ept = term(t, "NPN", "connector2"), term(t, "NPN", "connector1"), term(t, "NPN", "connector0")
+ sch_poly([(ks[0][0], yrl), (ks[1][0], yrl)])
+ sch_poly([(ks[1][0], yrl), (ks[2][0], yrl)])
+ sch_poly([(ks[2][0], yrl), (cpt[0], yrl), cpt], end=(t, "connector2"))
+ rb = res_base[r]
+ # patte 1 (connector0) côté base, patte 2 côté Arduino
+ place_sch(rb, "RES", "connector0", (bpt[0] - 9.0, bpt[1]), rot=180, size=SCH["RES_SIZE"])
+ rbase, rard = term(rb, "RES", "connector0"), term(rb, "RES", "connector1")
+ sch_poly([rbase, bpt], start=(rb, "connector0"), end=(t, "connector1"))
+ pin = f"D{11 + r}"
+ px, py = apin(pin)
+ sch_poly([(px, py), (XV[r], py), (XV[r], rard[1]), rard], start=ARDP(pin), end=(rb, "connector1"))
+ sch_poly([ept, (X_GND_S, ept[1])], start=(t, "connector0"))
+ yE = ept[1]
+ if r == 0:
+ y_e0 = yE
+ else:
+ sch_poly([(X_GND_S, prev_e), (X_GND_S, yE)])
+ prev_e = yE
+gx, gy = apin("GND")
+sch_poly([(X_GND_S, prev_e), (X_GND_S, Y_GND_S), (gx, Y_GND_S), (gx, gy)], end=ARDP("GND"))
+
+for pt, lst in ends.items():
+ for i in range(len(lst)):
+ for j in range(i + 1, len(lst)):
+ link("schematicView", lst[i][0], lst[i][1], "schematicTrace", lst[j][0],
+ lst[j][1], "schematicTrace")
+
+# ---------------------------------------------------------------- écriture XML
+def fmt(v):
+ return f"{v:.4f}".rstrip("0").rstrip(".") if isinstance(v, float) else str(v)
+
+def connects_xml(inst_id, view, con, indent):
+ out = []
+ for (oid, ocon, olayer) in conns.get((inst_id, view, con), []):
+ out.append(f'{indent}')
+ return "\n".join(out)
+
+def part_xml(inst):
+ L = []
+ L.append(f'')
+ for k, v in inst["props"].items():
+ L.append(f' ')
+ L.append(f' {escape(inst["title"])}')
+ L.append(' ')
+ for view, g in inst["views"].items():
+ L.append(f' <{view} layer="{g["layer"]}">')
+ if g.get("m"):
+ m11, m12, m21, m22, m31, m32 = g["m"]
+ L.append(f' ')
+ L.append(f' ')
+ L.append(' ')
+ else:
+ L.append(f' ')
+ cons = sorted({c for (i, v, c) in conns if i == inst["id"] and v == view}
+ | (set(inst.get("legs", {})) if view == "breadboardView" else set()))
+ if cons:
+ L.append(' ')
+ for con in cons:
+ L.append(f' ')
+ if view == "breadboardView" and con in inst.get("legs", {}):
+ (sx, sy), (lx, ly) = inst["legs"][con]
+ L.append(f' ')
+ L.append(' '
+ f'')
+ else:
+ L.append(' ')
+ L.append(' ')
+ L.append(connects_xml(inst["id"], view, con, " "))
+ L.append(' ')
+ L.append(' ')
+ L.append(' ')
+ L.append(f' {view}>')
+ L.append(' ')
+ L.append('')
+ return "\n".join(L)
+
+def wire_xml(w):
+ view, layer = w["view"], w["layer"]
+ L = [f'',
+ f' {escape(w["title"])}', ' ',
+ f' <{view} layer="{layer}">',
+ f' ',
+ f' ',
+ ' ']
+ for k in ("connector0", "connector1"):
+ L.append(f' ')
+ L.append(' ')
+ L.append(' ')
+ L.append(connects_xml(w["id"], view, k, " "))
+ L.append(' ')
+ L.append(' ')
+ L += [' ', f' {view}>', ' ', '']
+ return "\n".join(L)
+
+def build(out_path, ino_path=None):
+ head = '''
+
+
+
+
+
+
+
+'''
+ body = "\n".join(part_xml(i) for i in instances) + "\n" + "\n".join(wire_xml(w) for w in wires)
+ xml = head + body + "\n\n\n"
+ name = os.path.splitext(os.path.basename(out_path))[0]
+ with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as z:
+ z.writestr(name + ".fz", xml)
+ if ino_path:
+ z.write(ino_path, os.path.basename(ino_path))
+
+if __name__ == "__main__" and sys.argv[1] != "/dev/null":
+ build(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)
+ print(len(instances), "pièces,", len(wires), "fils")
diff --git a/matrice_3x3/outils/render_plaque.py b/matrice_3x3/outils/render_plaque.py
new file mode 100644
index 0000000..e756217
--- /dev/null
+++ b/matrice_3x3/outils/render_plaque.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+"""Plan d'implantation vu de dessus : côté composants + côté soudures (miroir)."""
+import runpy, sys
+OUT = sys.argv[1]
+sys.argv = ["x", "/dev/null"]
+g = runpy.run_path(sys.path[0] + "/gen_fzz.py", run_name="gen")
+COLS, ROWS = g["COLS"], g["ROWS"]
+NU = g["NU"]
+leds, res_anode, res_base, trans = g["leds"], g["res_anode"], g["res_base"], g["trans"]
+board_wires, ard_jobs = g["board_wires"], g["ard_jobs"]
+
+PX = 16 # pas d'affichage
+MARGE_X, MARGE_Y = 44, 120
+PANNEAU = COLS * PX + 2 * MARGE_X
+LARG = 2 * PANNEAU + 40
+HAUT = MARGE_Y + ROWS * PX + 270
+out = []
+A = out.append
+
+def xy(c, r, panneau):
+ if panneau == 1: # côté soudures : plaque retournée gauche/droite
+ c = COLS - 1 - c
+ return (panneau * (PANNEAU + 40) + MARGE_X + c * PX + PX / 2, MARGE_Y + r * PX + PX / 2)
+
+def txt(x, y, s, size=11, anchor="middle", fill="#333", weight="normal", rot=None):
+ tr = f' transform="rotate({rot} {x:.1f} {y:.1f})"' if rot else ""
+ A(f'{s}')
+
+A(f'")
+with open(OUT, "w") as f:
+ f.write("\n".join(out))
diff --git a/matrice_3x3/outils/schema_electrique.py b/matrice_3x3/outils/schema_electrique.py
new file mode 100644
index 0000000..1e85dee
--- /dev/null
+++ b/matrice_3x3/outils/schema_electrique.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+"""Dessine schemas/schema_electrique.svg : schéma de la maquette 3 x 3,
+avec les symboles normalisés (résistances rectangulaires) et des fils colorés.
+Usage : python3 outils/schema_electrique.py schemas/schema_electrique.svg"""
+import sys
+
+ROUGE, VERT, BLEU = "#d62828", "#2a9d3a", "#2f6fc4"
+TEINTE = {ROUGE: "#fbd5d5", VERT: "#d3f0d8", BLEU: "#d6e3f7"}
+LIGNE, BASE, MASSE, TEXTE = "#3a3a3a", "#c98a00", "#222222", "#333333"
+
+LX = [170, 370, 570] # x de la diode rouge de chaque colonne
+LY = [330, 480, 630] # y des anodes de chaque ligne
+DX = 48 # écart entre les diodes R, G, B d'une LED
+COULEURS = [("R", ROUGE, "220 Ω"), ("G", VERT, "150 Ω"), ("B", BLEU, "150 Ω")]
+XT = 790 # centre des transistors
+XD = {0: 900, 1: 930, 2: 960} # descentes D11, D12, D13
+X_GND = 1000
+Y_PIN = 170 # bas du bloc Arduino
+
+out = []
+A = out.append
+
+def ligne(x1, y1, x2, y2, c, w=2.2):
+ A(f'')
+
+def chemin(d, c, w=2.2):
+ A(f'')
+
+def point(x, y, c):
+ A(f'')
+
+def texte(x, y, s, taille=13, ancre="middle", c=TEXTE, gras=False, rot=None, italique=False):
+ tr = f' transform="rotate({rot} {x} {y})"' if rot else ""
+ A(f'{s}')
+
+def resistance_v(x, y1, y2, c, valeur):
+ """Résistance verticale entre y1 et y2 (rectangle de 50 px au milieu)."""
+ m = (y1 + y2) / 2
+ ligne(x, y1, x, m - 25, c)
+ ligne(x, m + 25, x, y2, c)
+ A(f'')
+ texte(x + 19, m, valeur, 12, c=c, rot=-90)
+
+def resistance_h(x1, x2, y, c, valeur):
+ m = (x1 + x2) / 2
+ ligne(x1, y, m - 25, y, c)
+ ligne(m + 25, y, x2, y, c)
+ A(f'')
+ texte(m, y - 13, valeur, 12, c=c)
+
+def diode(x, ya, c):
+ """Diode verticale : anode en haut (ya), cathode en bas (ya + 32)."""
+ ligne(x, ya, x, ya + 8, c)
+ A(f'')
+ ligne(x - 9, ya + 22, x + 9, ya + 22, c, 2.4)
+ ligne(x, ya + 22, x, ya + 32, LIGNE)
+ for dy in (0, 7): # lumière émise
+ x1, y1, x2, y2 = x + 9, ya + 15 + dy, x + 18, ya + 6 + dy
+ ligne(x1, y1, x2, y2, c, 1.4)
+ A(f'')
+
+def masse(x, y):
+ ligne(x - 11, y, x + 11, y, MASSE, 2.4)
+ ligne(x - 7, y + 5, x + 7, y + 5, MASSE, 2.4)
+ ligne(x - 3, y + 10, x + 3, y + 10, MASSE, 2.4)
+
+def transistor(r, yt):
+ """NPN dessiné en miroir : base à droite, collecteur en haut à gauche,
+ émetteur en bas à gauche (vers la masse)."""
+ xb = XT + 8
+ A(f'')
+ ligne(xb, yt - 13, xb, yt + 13, TEXTE, 3)
+ ligne(xb, yt - 6, XT - 12, yt - 20, LIGNE) # collecteur
+ ligne(xb, yt + 6, XT - 12, yt + 20, MASSE) # émetteur
+ A(f'')
+ ligne(xb, yt, XT + 40, yt, BASE) # base
+ texte(XT - 44, yt - 2, f"T{r}", 14, gras=True)
+ texte(XT - 44, yt + 14, "BC337", 11, c="#666")
+ texte(XT + 22, yt + 18, "B", 10, c="#888")
+ texte(XT - 22, yt - 26, "C", 10, c="#888")
+ texte(XT - 26, yt + 28, "E", 10, c="#888")
+
+W, H = 1080, 970
+A(f'")
+
+with open(sys.argv[1], "w") as f:
+ f.write("\n".join(out))
diff --git a/matrice_3x3/schemas/implantation_plaque.svg b/matrice_3x3/schemas/implantation_plaque.svg
new file mode 100644
index 0000000..9ba611c
--- /dev/null
+++ b/matrice_3x3/schemas/implantation_plaque.svg
@@ -0,0 +1,2626 @@
+
\ No newline at end of file
diff --git a/matrice_3x3/schemas/plaque_fritzing.png b/matrice_3x3/schemas/plaque_fritzing.png
new file mode 100644
index 0000000..9e346af
Binary files /dev/null and b/matrice_3x3/schemas/plaque_fritzing.png differ
diff --git a/matrice_3x3/schemas/schema_electrique.svg b/matrice_3x3/schemas/schema_electrique.svg
new file mode 100644
index 0000000..09be2ec
--- /dev/null
+++ b/matrice_3x3/schemas/schema_electrique.svg
@@ -0,0 +1,501 @@
+
\ No newline at end of file
diff --git a/matrice_3x3/schemas/schema_fritzing.png b/matrice_3x3/schemas/schema_fritzing.png
new file mode 100644
index 0000000..7464cdc
Binary files /dev/null and b/matrice_3x3/schemas/schema_fritzing.png differ