better 3x3 doc

This commit is contained in:
bach
2026-10-08 14:36:28 +02:00
parent 3a0addaa55
commit 32e617f67b
9 changed files with 4303 additions and 0 deletions
+489
View File
@@ -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}<connect connectorId="{ocon}" modelIndex="{oid}" layer="{olayer}"/>')
return "\n".join(out)
def part_xml(inst):
L = []
L.append(f'<instance moduleIdRef="{escape(inst["module"])}" modelIndex="{inst["id"]}" '
f'path="{escape(inst["path"])}">')
for k, v in inst["props"].items():
L.append(f' <property name="{k}" value="{escape(v)}"/>')
L.append(f' <title>{escape(inst["title"])}</title>')
L.append(' <views>')
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' <geometry z="{g["z"]}" x="{fmt(g["x"])}" y="{fmt(g["y"])}">')
L.append(f' <transform m11="{fmt(m11)}" m12="{fmt(m12)}" m13="0" '
f'm21="{fmt(m21)}" m22="{fmt(m22)}" m23="0" m31="{fmt(m31)}" '
f'm32="{fmt(m32)}" m33="1"/>')
L.append(' </geometry>')
else:
L.append(f' <geometry z="{g["z"]}" x="{fmt(g["x"])}" y="{fmt(g["y"])}"/>')
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(' <connectors>')
for con in cons:
L.append(f' <connector connectorId="{con}" layer="{g["layer"]}">')
if view == "breadboardView" and con in inst.get("legs", {}):
(sx, sy), (lx, ly) = inst["legs"][con]
L.append(f' <geometry x="{fmt(sx)}" y="{fmt(sy)}"/>')
L.append(' <leg><point x="0" y="0"/><bezier/>'
f'<point x="{fmt(lx)}" y="{fmt(ly)}"/><bezier/></leg>')
else:
L.append(' <geometry x="0" y="0"/>')
L.append(' <connects>')
L.append(connects_xml(inst["id"], view, con, " "))
L.append(' </connects>')
L.append(' </connector>')
L.append(' </connectors>')
L.append(f' </{view}>')
L.append(' </views>')
L.append('</instance>')
return "\n".join(L)
def wire_xml(w):
view, layer = w["view"], w["layer"]
L = [f'<instance moduleIdRef="WireModuleID" modelIndex="{w["id"]}" '
f'path=":/resources/parts/core/wire.fzp">',
f' <title>{escape(w["title"])}</title>', ' <views>',
f' <{view} layer="{layer}">',
f' <geometry z="{w.get("z", 3.5)}" x="{fmt(w["x"])}" y="{fmt(w["y"])}" x1="0" y1="0" '
f'x2="{fmt(w["dx"])}" y2="{fmt(w["dy"])}" wireFlags="{w["flags"]}"/>',
f' <wireExtras mils="{w.get("mils", "22.2222")}" color="{w["color"]}" '
f'opacity="1" banded="0"/>',
' <connectors>']
for k in ("connector0", "connector1"):
L.append(f' <connector connectorId="{k}" layer="{layer}">')
L.append(' <geometry x="0" y="0"/>')
L.append(' <connects>')
L.append(connects_xml(w["id"], view, k, " "))
L.append(' </connects>')
L.append(' </connector>')
L += [' </connectors>', f' </{view}>', ' </views>', '</instance>']
return "\n".join(L)
def build(out_path, ino_path=None):
head = '''<?xml version="1.0" encoding="UTF-8"?>
<module fritzingVersion="1.0.5" icon=".png">
<views>
<view name="breadboardView" backgroundColor="#ffffff" gridSize="0.1in" showGrid="1" alignToGrid="1" viewFromBelow="0" colorWiresByLength="0"/>
<view name="schematicView" backgroundColor="#ffffff" gridSize="0.1in" showGrid="1" alignToGrid="1" viewFromBelow="0"/>
<view name="pcbView" backgroundColor="#333333" gridSize="0.05in" showGrid="1" alignToGrid="1" viewFromBelow="0"/>
</views>
<instances>
'''
body = "\n".join(part_xml(i) for i in instances) + "\n" + "\n".join(wire_xml(w) for w in wires)
xml = head + body + "\n</instances>\n</module>\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")
+200
View File
@@ -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'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" text-anchor="{anchor}" '
f'fill="{fill}" font-weight="{weight}"{tr}>{s}</text>')
A(f'<svg xmlns="http://www.w3.org/2000/svg" width="{LARG}" height="{HAUT}" '
f'viewBox="0 0 {LARG} {HAUT}" font-family="Helvetica, Arial, sans-serif">')
A('<title>Implantation de la matrice 3x3 sur plaque à pastilles</title>')
A(f'<rect width="{LARG}" height="{HAUT}" fill="#ffffff"/>')
for pan, titre, sous in ((0, "Côté composants (vu de dessus)",
"on pose les composants et les fils isolés"),
(1, "Côté soudures (plaque retournée)",
"on soude et on relie avec du fil nu. Attention : image en miroir !")):
x0 = pan * (PANNEAU + 40) + MARGE_X
txt(x0, 34, titre, 18, "start", "#222", "bold")
txt(x0, 54, sous, 13, "start", "#666")
# plaque
A(f'<rect x="{x0 - 8}" y="{MARGE_Y - 8}" width="{COLS * PX + 16}" height="{ROWS * PX + 16}" '
f'rx="6" fill="#e3c58f" stroke="#b8975c"/>')
# numéros de colonnes et de lignes (comptés à partir de 1)
for c in range(COLS):
x, _ = xy(c, 0, pan)
if (c + 1) % 5 == 0 or c == 0:
txt(x, MARGE_Y + ROWS * PX + 22, str(c + 1), 10, fill="#8a6d3b")
for r in range(ROWS):
_, y = xy(0, r, pan)
if (r + 1) % 5 == 0 or r == 0:
txt(x0 - 14, y + 4, str(r + 1), 10, fill="#8a6d3b")
# pastilles
for c in range(COLS):
for r in range(ROWS):
x, y = xy(c, r, pan)
A(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="4.6" fill="#c9a063"/>'
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="1.8" fill="#fff8ec"/>')
# ---------------------------------------------------------------- côté composants
def contour_led(inst, pan, plein):
hs = list(inst["holes"].values())
xs = [xy(c, r, pan)[0] for c, r in hs]
cx, cy = sum(xs) / 4, xy(*hs[0], pan)[1]
if plein:
A(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{PX * 1.0:.1f}" fill="#ffffff" '
f'fill-opacity="0.85" stroke="#555" stroke-width="1.5"/>')
for (c, r), lettre, coul in zip(hs, "RKGB", ("#cc1414", "#222", "#2a9d3a", "#2f6fc4")):
x, y = xy(c, r, pan)
A(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="2.6" fill="{coul}"/>')
txt(x, y - 6, lettre, 9, fill=coul, weight="bold")
else:
A(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{PX * 1.0:.1f}" fill="none" '
f'stroke="#9a7b4a" stroke-dasharray="3 3"/>')
def contour_res(inst, pan, plein, valeur):
(c1, r1), (c2, r2) = inst["holes"].values()
(xa, ya), (xb, yb) = xy(c1, r1, pan), xy(c2, r2, pan)
if plein:
A(f'<line x1="{xa:.1f}" y1="{ya:.1f}" x2="{xb:.1f}" y2="{yb:.1f}" stroke="#8c8c8c" '
f'stroke-width="2"/>')
vertical = abs(xa - xb) < 1
L = 0.55 * (abs(ya - yb) if vertical else abs(xa - xb))
mx, my = (xa + xb) / 2, (ya + yb) / 2
w, h = (9, L) if vertical else (L, 9)
style = ('fill="#e8d3a8" stroke="#a88a52"' if plein
else 'fill="none" stroke="#9a7b4a" stroke-dasharray="3 3"')
A(f'<rect x="{mx - w / 2:.1f}" y="{my - h / 2:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'rx="3" {style}/>')
if plein:
txt(mx + (0 if vertical else 0), my + (3 if not vertical else 3), valeur, 8,
fill="#5a4310", weight="bold", rot=(-90 if vertical else None))
def contour_npn(inst, pan, plein, nom):
hs = inst["holes"]
xc, y = xy(*hs["connector2"], pan)
xe, _ = xy(*hs["connector0"], pan)
cx = (xc + xe) / 2
R = PX * 1.25
# demi-cercle, face plate vers le bas de la plaque
d = f'M{cx - R:.1f} {y + 5:.1f} A{R:.1f} {R:.1f} 0 0 1 {cx + R:.1f} {y + 5:.1f} Z'
if plein:
A(f'<path d="{d}" fill="#2b2b2b" fill-opacity="0.9"/>')
for con, lettre in (("connector2", "C"), ("connector1", "B"), ("connector0", "E")):
x, yy = xy(*hs[con], pan)
A(f'<circle cx="{x:.1f}" cy="{yy:.1f}" r="2.6" fill="#ddd"/>')
txt(x, yy - 6, lettre, 9, fill="#fff", weight="bold")
txt(cx, y - R + 2, nom, 10, fill="#222", weight="bold")
else:
A(f'<path d="{d}" fill="none" stroke="#9a7b4a" stroke-dasharray="3 3"/>')
def composants(pan, plein):
for k, inst in res_anode.items():
contour_res(inst, pan, plein, inst["props"]["resistance"].replace("Ω", ""))
for r, inst in res_base.items():
contour_res(inst, pan, plein, "1k")
for inst in leds.values():
contour_led(inst, pan, plein)
for r, inst in trans.items():
contour_npn(inst, pan, plein, f"T{r}")
# fils isolés (côté composants)
for a, b, coul in board_wires:
if coul == NU:
continue
(xa, ya), (xb, yb) = xy(*a, 0), xy(*b, 0)
A(f'<line x1="{xa:.1f}" y1="{ya:.1f}" x2="{xb:.1f}" y2="{yb:.1f}" stroke="{coul}" '
f'stroke-width="5" stroke-linecap="round"/>')
composants(0, True)
# arrivées des fils de l'Arduino
for pin, (c, r), coul in ard_jobs:
x, y = xy(c, r, 0)
A(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="5.5" fill="none" stroke="{coul}" stroke-width="3"/>')
if r <= 2: # entrées en haut de la plaque : étiquette verticale au-dessus
A(f'<line x1="{x:.1f}" y1="{y - 6:.1f}" x2="{x:.1f}" y2="{MARGE_Y - 26:.1f}" '
f'stroke="{coul}" stroke-width="2"/>')
txt(x + 4, MARGE_Y - 30, pin, 11, "start", coul if coul != "#ffe24d" else "#b39b00",
"bold", rot=-90)
else:
txt(x + 9, y + 4, pin, 11, "start", "#222", "bold")
# ---------------------------------------------------------------- côté soudures
composants(1, False)
for a, b, coul in board_wires:
(xa, ya), (xb, yb) = xy(*a, 1), xy(*b, 1)
if coul == NU:
A(f'<line x1="{xa:.1f}" y1="{ya:.1f}" x2="{xb:.1f}" y2="{yb:.1f}" stroke="#7d7d7d" '
f'stroke-width="4" stroke-linecap="round"/>')
soudures = set()
for inst in list(leds.values()) + list(res_anode.values()) + list(res_base.values()) + list(trans.values()):
soudures.update(inst["holes"].values())
for a, b, coul in board_wires:
soudures.update((a, b))
for _, h, _ in ard_jobs:
soudures.add(h)
for c, r in soudures:
x, y = xy(c, r, 1)
A(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="4.4" fill="#bfbfbf" stroke="#6f6f6f"/>')
for a, b, coul in board_wires: # bouts des fils isolés qui traversent
if coul != NU:
for h in (a, b):
x, y = xy(*h, 1)
A(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="2.2" fill="{coul}"/>')
for pin, h, coul in ard_jobs:
x, y = xy(*h, 1)
A(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="2.2" fill="{coul}"/>')
# ---------------------------------------------------------------- légende
ly = MARGE_Y + ROWS * PX + 56
txt(MARGE_X, ly, "Légende", 15, "start", "#222", "bold")
items = [
('<circle cx="{x}" cy="{y}" r="9" fill="#fff" stroke="#555" stroke-width="1.5"/>',
"LED RGB vue de dessus : pattes R, K (cathode, la plus longue), G, B"),
('<rect x="{xm}" y="{ym}" width="22" height="9" rx="3" fill="#e8d3a8" stroke="#a88a52"/>',
"Résistance (valeur en ohms : 220, 150, 1k = 1000)"),
('<path d="M{xl} {y4} A11 11 0 0 1 {xr} {y4} Z" fill="#2b2b2b"/>',
"Transistor BC337, face plate vers le bas : pattes C, B, E de gauche à droite"),
('<line x1="{xl}" y1="{y}" x2="{xr}" y2="{y}" stroke="#404040" stroke-width="5" stroke-linecap="round"/>',
"Fil isolé, côté composants (cathodes des lignes)"),
('<line x1="{xl}" y1="{y}" x2="{xr}" y2="{y}" stroke="#7d7d7d" stroke-width="4" stroke-linecap="round"/>',
"Fil nu (ou patte repliée), côté soudures"),
('<circle cx="{x}" cy="{y}" r="5.5" fill="none" stroke="#cc1414" stroke-width="3"/>',
"Arrivée d'un fil vers l'Arduino (D2 à D13, GND)"),
]
for i, (forme, texte) in enumerate(items):
col, lig = i % 2, i // 2
bx = MARGE_X + col * (LARG / 2)
by = ly + 30 + lig * 30
A(forme.format(x=bx + 11, y=by - 4, xm=bx, ym=by - 8, xl=bx, xr=bx + 22, y4=by + 2))
txt(bx + 34, by, texte, 12, "start", "#333")
txt(MARGE_X, ly + 140, "Pas entre LED : 6 trous = 15,24 mm. Les numéros de colonnes et de "
"lignes sont ceux du côté composants ; côté soudures, gauche et droite sont inversées.",
12, "start", "#666")
txt(MARGE_X, ly + 160, "Correspondance avec le code : colonne c de la matrice = LED Lr Cc ; "
"ligne r = transistor Tr (broche D11 + r).", 12, "start", "#666")
A("</svg>")
with open(OUT, "w") as f:
f.write("\n".join(out))
+194
View File
@@ -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'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{c}" stroke-width="{w}" '
f'stroke-linecap="round"/>')
def chemin(d, c, w=2.2):
A(f'<path d="{d}" fill="none" stroke="{c}" stroke-width="{w}" stroke-linecap="round" '
f'stroke-linejoin="round"/>')
def point(x, y, c):
A(f'<circle cx="{x}" cy="{y}" r="4" fill="{c}"/>')
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'<text x="{x}" y="{y}" font-size="{taille}" text-anchor="{ancre}" fill="{c}"'
f'{" font-weight=\"bold\"" if gras else ""}{" font-style=\"italic\"" if italique else ""}'
f'{tr}>{s}</text>')
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'<rect x="{x - 7}" y="{m - 25}" width="14" height="50" rx="2" fill="#fff" '
f'stroke="{c}" stroke-width="2.2"/>')
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'<rect x="{m - 25}" y="{y - 7}" width="50" height="14" rx="2" fill="#fff" '
f'stroke="{c}" stroke-width="2.2"/>')
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'<path d="M{x - 9} {ya + 8} L{x + 9} {ya + 8} L{x} {ya + 22} Z" fill="{TEINTE[c]}" '
f'stroke="{c}" stroke-width="2" stroke-linejoin="round"/>')
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'<path d="M{x2} {y2} l-5 1 l4 4 Z" fill="{c}"/>')
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'<circle cx="{XT - 1}" cy="{yt}" r="25" fill="#fafafa" stroke="{TEXTE}" stroke-width="1.6"/>')
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'<path d="M{XT - 12} {yt + 20} l3 -9 l5 7 Z" fill="{MASSE}"/>')
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'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}" '
f'font-family="Helvetica, Arial, sans-serif">')
A('<title>Schéma électrique de la maquette 3 x 3</title>')
A(f'<rect width="{W}" height="{H}" fill="#ffffff"/>')
texte(40, 38, "Maquette 3 × 3 : schéma électrique", 22, "start", "#111", True)
texte(40, 60, "9 LED RGB à cathode commune, allumées ligne par ligne (multiplexage)",
14, "start", "#666")
# ---------------------------------------------------------------- Arduino
A(f'<rect x="100" y="80" width="930" height="{Y_PIN - 80}" rx="10" fill="#e8f4f5" '
f'stroke="#00878f" stroke-width="2.4"/>')
texte(565, 112, "Arduino Uno", 20, gras=True, c="#00666d")
texte(565, 132, "alimenté par le câble USB", 12, c="#4d8f94")
broches = {}
for c in range(3):
for k, (_, coul, _) in enumerate(COULEURS):
broches[f"D{2 + 3 * c + k}"] = (LX[c] + DX * k - 22, coul)
for r in range(3):
broches[f"D{11 + r}"] = (XD[r], BASE)
broches["GND"] = (X_GND, MASSE)
for nom, (x, coul) in broches.items():
ligne(x, Y_PIN, x, Y_PIN + 20, coul)
texte(x, Y_PIN - 10, nom, 12, gras=True, c=coul if coul != BASE else "#9a6a00")
masse(X_GND, Y_PIN + 20)
# ---------------------------------------------------------------- colonnes (anodes)
for c in range(3):
for k, (_, coul, val) in enumerate(COULEURS):
xb = LX[c] + DX * k - 22
resistance_v(xb, Y_PIN + 20, 265, coul, val)
y_fin = LY[2] - 12
ligne(xb, 265, xb, y_fin, coul)
for r in range(3):
xd, ys = LX[c] + DX * k, LY[r] - 12
chemin(f"M{xb} {ys} H{xd} V{LY[r]}", coul)
if r < 2:
point(xb, ys, coul)
texte(LX[c] + DX, LY[2] + 100, f"colonne {c}", 14, c="#555", gras=True)
# ---------------------------------------------------------------- LED
for r in range(3):
for c in range(3):
x0 = LX[c]
A(f'<rect x="{x0 - 14}" y="{LY[r] - 4}" width="{2 * DX + 28}" height="40" rx="8" '
f'fill="#f2f2f2"/>')
texte(x0 + DX + 2, LY[r] - 22, f"L{r}C{c}", 11, c="#888", gras=True)
for r in range(3):
for c in range(3):
for k, (_, coul, _) in enumerate(COULEURS):
diode(LX[c] + DX * k, LY[r], coul)
# ---------------------------------------------------------------- lignes (cathodes)
bus_x = sorted(LX[c] + DX * k - 22 for c in range(3) for k in range(3))
for r in range(3):
y = LY[r] + 60
for c in range(3):
for k in range(3):
xd = LX[c] + DX * k
ligne(xd, LY[r] + 32, xd, y, LIGNE)
if not (c == 0 and k == 0):
point(xd, y, LIGNE)
# ligne horizontale avec des « ponts » là où elle croise une colonne
d = f"M{LX[0]} {y}"
if r < 2:
for xb in bus_x:
if LX[0] < xb < XT - 12:
d += f" H{xb - 6} a6 6 0 0 1 12 0"
d += f" H{XT - 12} V{y + 40 - 20}"
chemin(d, LIGNE)
texte(XT - 56, y - 8, f"ligne {r}", 13, "end", c="#555", gras=True)
yt = y + 40
transistor(r, yt)
ligne(XT - 12, yt + 20, XT - 12, yt + 36, MASSE)
masse(XT - 12, yt + 36)
resistance_h(XT + 40, XD[r], yt, BASE, "1 kΩ")
ligne(XD[r], yt, XD[r], Y_PIN + 20, BASE)
# ---------------------------------------------------------------- légende
ly = 840
A(f'<rect x="40" y="{ly - 26}" width="{W - 80}" height="136" rx="10" fill="#fafafa" stroke="#e2e2e2"/>')
texte(60, ly, "Légende", 15, "start", "#111", True)
items = [
(lambda x, y: (ligne(x, y, x + 30, y, ROUGE), ligne(x + 34, y, x + 64, y, VERT),
ligne(x + 68, y, x + 98, y, BLEU)),
"colonnes : anodes R, G, B (D2 à D10)"),
(lambda x, y: ligne(x, y, x + 98, y, LIGNE), "lignes : cathodes communes"),
(lambda x, y: ligne(x, y, x + 98, y, BASE), "commande des lignes (D11 à D13)"),
(lambda x, y: masse(x + 49, y - 6), "masse (GND) : symboles tous reliés"),
(lambda x, y: point(x + 49, y, LIGNE), "point = fils connectés"),
(lambda x, y: chemin(f"M{x} {y} H{x + 43} a6 6 0 0 1 12 0 H{x + 98}", LIGNE),
"pont = croisement sans contact"),
]
for i, (dessin, legende) in enumerate(items):
col, lig = i % 3, i // 3
x, y = (60, 400, 700)[col], ly + 30 + lig * 32
dessin(x, y)
texte(x + 110, y + 4, legende, 12, "start")
texte(60, ly + 98, "Un pixel s'allume quand sa ligne est active (D11, D12 ou D13 à HIGH : le "
"transistor relie les cathodes à la masse) et que sa couleur l'est aussi (D2 à D10).",
12, "start", "#555", italique=True)
A("</svg>")
with open(sys.argv[1], "w") as f:
f.write("\n".join(out))