#!/usr/bin/env python3 # coding=utf-8 """ Genere le schema explicatif des parametres de l'extension. python docs/schema_parametres.py # les deux versions python docs/schema_parametres.py fr # docs/parametres.svg + .png (README) python docs/schema_parametres.py en # docs/parameters_en.svg # + images/parameters_en.png (onglet # « Help » de la boite de dialogue) Les formes sont calculees par living_hinge_core (memes fonctions que l'extension), le schema reste donc fidele au resultat reel. L'export PNG passe par inkscape.com, qui attend la fin de l'export (contrairement au raccourci Chocolatey « inkscape »). """ import math import os import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) sys.path.insert(0, ROOT) import living_hinge_core as core # noqa: E402 W, H = 1400, 1280 # 4 panneaux de 700 x 640 INK = "#343a40" # element principal GHOST = "#dde1e5" # elements secondaires / voisins DIM = "#d9480f" # cotes DIM2 = "#1971c2" # cotes secondaires EDGE = "#212529" # contour des formes FONT = "font-family:Arial,Helvetica,sans-serif" INKSCAPE = [r"C:\Program Files\Inkscape\bin\inkscape.com", "/usr/bin/inkscape", "/Applications/Inkscape.app/Contents/MacOS/inkscape"] # Version par langue : textes, fichiers produits, largeur du PNG et agrandissement # des textes (la version anglaise est affichee reduite dans la boite de dialogue : # 900 px de large, textes x 1.25 pour rester lisibles). VERSIONS = { "fr": { "svg": os.path.join(HERE, "parametres.svg"), "png": os.path.join(HERE, "parametres.png"), "png_width": 1400, "font_scale": 1.0, "text": { "title1": "Lumière : longueur et largeur", "note1": ["Chaque lumière est un rectangle à bouts arrondis ;", "la longueur se mesure hors tout."], "length": "longueur", "width": "largeur", "title2": "Pont, pas et décalage", "note2": ["Pont : matière entre deux lumières d'une colonne. Pas : entraxe", "des colonnes. Décalage en % de (longueur + pont), 50 % = quinconce."], "bridge": "pont", "pitch": "pas", "title3": "Longueurs aléatoires et angle", "note3": ["Aléatoire : longueurs au hasard entre le mini et la longueur,", "colonnes pleines d'une marge à l'autre. L'angle tourne le motif."], "shortest": "mini", "longest": "longueur", "stagger": "50 %", "angle": "angle", "title4": "Marge et longueur minimale", "note4": ["Près du bord et des trous, les lumières sont raccourcies à", "la marge ; plus courtes que le minimum, elles disparaissent."], "margin": "marge", "minimum": "≥ mini", }, }, "en": { "svg": os.path.join(HERE, "parameters_en.svg"), "png": os.path.join(ROOT, "images", "parameters_en.png"), "png_width": 900, "font_scale": 1.25, "text": { "title1": "Slot: length and width", "note1": ["Each slot is a rectangle with rounded ends;", "its length is measured end to end."], "length": "length", "width": "width", "title2": "Bridge, pitch and stagger", "note2": ["Bridge: material between two slots of a column. Pitch: column", "spacing. Stagger in % of (length + bridge), 50 % = staggered."], "bridge": "bridge", "pitch": "pitch", "title3": "Random lengths and angle", "note3": ["Random: lengths drawn between the shortest and the length,", "columns filled from margin to margin. The angle turns the pattern."], "shortest": "min", "longest": "length", "stagger": "50 %", "angle": "angle", "title4": "Margin and minimum length", "note4": ["Near the edge and holes, slots are shortened to the", "margin; shorter than the minimum, they are dropped."], "margin": "margin", "minimum": "≥ min", }, }, } def _unit(angle): """Vecteur unitaire, angle en degres, sens anti-horaire a l'ecran (y vers le bas).""" a = math.radians(angle) return math.cos(a), -math.sin(a) out = [] T = {} FS = 1.0 def fmt(v): return "{:.2f}".format(v).rstrip("0").rstrip(".") def pts(polyline): return " ".join("{},{}".format(fmt(x), fmt(y)) for x, y in polyline) def polyline(points, color, width, extra=""): out.append(''.format(pts(points), color, fmt(width), extra)) def motif(polylines, color, width): for pl in polylines: polyline(pl, color, width) def text(x, y, s, size=17, color=INK, anchor="start", weight="normal", halo=True): style = "{};font-size:{}px;font-weight:{};fill:{}".format( FONT, fmt(size * FS), weight, color) if halo: style += ";paint-order:stroke;stroke:#ffffff;stroke-width:5px;stroke-linejoin:round" out.append('{}'.format( fmt(x), fmt(y), anchor, style, s)) def note(x, y, lines): """Legende de bas de panneau, derniere ligne sur la ligne de base `y`.""" step = 22 * FS for k, line in enumerate(lines): text(x, y - (len(lines) - 1 - k) * step, line, size=16, halo=False) def arrow_head(tip, direction, color, size=9): ux, uy = direction nx, ny = -uy, ux base = (tip[0] - size * ux, tip[1] - size * uy) out.append(''.format(pts([ tip, (base[0] + 0.45 * size * nx, base[1] + 0.45 * size * ny), (base[0] - 0.45 * size * nx, base[1] - 0.45 * size * ny)]), color)) def dimension(p, q, offset, label, color=DIM, label_pos=0.5, label_shift=18, size=17): """Cote entre p et q, decalee de `offset` perpendiculairement.""" dx, dy = q[0] - p[0], q[1] - p[1] length = math.hypot(dx, dy) ux, uy = dx / length, dy / length nx, ny = -uy, ux a = (p[0] + offset * nx, p[1] + offset * ny) b = (q[0] + offset * nx, q[1] + offset * ny) sign = 1 if offset >= 0 else -1 for base, end in ((p, a), (q, b)): polyline([(base[0] + sign * 4 * nx, base[1] + sign * 4 * ny), (end[0] + sign * 6 * nx, end[1] + sign * 6 * ny)], color, 1, 'stroke-dasharray="3,3"') polyline([a, b], color, 1.6) arrow_head(a, (-ux, -uy), color) arrow_head(b, (ux, uy), color) lx = a[0] + label_pos * (b[0] - a[0]) + sign * label_shift * FS * nx ly = a[1] + label_pos * (b[1] - a[1]) + sign * label_shift * FS * ny + 6 * FS text(lx, ly, label, size=size, color=color, anchor="middle", weight="bold") def arc(center, radius, a0, a1, color, label=None, label_radius=None, size=16): """Arc de a0 a a1 degres (sens anti-horaire a l'ecran), fleche au bout.""" steps = 40 points = [] for k in range(steps + 1): ux, uy = _unit(a0 + (a1 - a0) * k / steps) points.append((center[0] + radius * ux, center[1] + radius * uy)) polyline(points[:-2], color, 1.6) last, before = points[-1], points[-3] d = math.hypot(last[0] - before[0], last[1] - before[1]) arrow_head(last, ((last[0] - before[0]) / d, (last[1] - before[1]) / d), color, 8) if label: ux, uy = _unit((a0 + a1) / 2) r = label_radius or radius + 24 text(center[0] + r * ux, center[1] + r * uy + 6 * FS, label, size=size, color=color, anchor="middle", weight="bold") def circle_ring(cx, cy, r, n=180): return [(cx + r * math.cos(2 * math.pi * k / n), cy + r * math.sin(2 * math.pi * k / n)) for k in range(n)] def panel_title(x, y, number, title): text(x, y, "{} {}".format(number, title), size=22, weight="bold", halo=False) # -------------------------------------------------------------------------- # Panneaux : un par groupe de parametres, numerotes 1 a 4. Les lumieres sont # calculees par le noyau (hinge_slots, slot_outline), en pixels du schema. # -------------------------------------------------------------------------- def rect_ring(x0, y0, x1, y1): return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)] def shape(rings, fill="#f1f3f5"): d = " ".join("M {} Z".format(" L ".join("{},{}".format(fmt(x), fmt(y)) for x, y in ring)) for ring in rings) out.append(''.format(d, fill, EDGE)) def draw_slots(slots, radius, color, width=2.5): for slot in slots: out.append(''.format( pts(core.slot_outline(slot, radius)), color, fmt(width))) def guide(p, q, color=DIM): polyline([p, q], color, 1, 'stroke-dasharray="3,3"') def nearest(slots, point): """Lumiere dont le milieu est le plus proche de `point`.""" return min(slots, key=lambda s: math.hypot((s[0][0] + s[1][0]) / 2 - point[0], (s[0][1] + s[1][1]) / 2 - point[1])) def panel_1(): panel_title(30, 50, 1, T["title1"]) slots = core.hinge_slots([rect_ring(140, 100, 560, 540)], length=300, width=60, bridge=50, pitch=150, min_length=60) main = nearest(slots, (350, 320)) draw_slots([s for s in slots if s is not main], 30, GHOST) draw_slots([main], 30, INK, 3) (x, y0), (_x, y1) = main dimension((x - 30, y0 - 30), (x - 30, y1 + 30), 45, T["length"], label_shift=48) dimension((x - 30, y0), (x + 30, y0), -75, T["width"]) note(30, 610, T["note1"]) def panel_2(): ox = 700 panel_title(ox + 30, 50, 2, T["title2"]) slots = core.hinge_slots([rect_ring(ox + 90, 90, ox + 590, 500)], length=200, width=44, bridge=50, pitch=100, min_length=44) draw_slots(slots, 22, INK) # Pont : entre la lumiere centrale de la colonne de gauche et celle du dessous. left = min(x for (x, _y), _p in slots) column = sorted(s for s in slots if s[0][0] == left) upper = nearest(column, (left, 295)) lower = column[column.index(upper) + 1] dimension((left - 22, upper[1][1] + 22), (left - 22, lower[0][1] - 22), 30, T["bridge"], label_shift=34) # Pas : entre les axes de deux colonnes voisines, sous le motif. dimension((ox + 340, 500), (ox + 440, 500), 25, T["pitch"]) # Decalage : entre le haut d'une lumiere paire et celui de sa voisine impaire. right = max(x for (x, _y), _p in slots) even = nearest([s for s in slots if s[0][0] == right], (right, 295)) odd = nearest([s for s in slots if s[0][0] == right - 100], (right - 100, 420)) top_even, top_odd = even[0][1] - 22, odd[0][1] - 22 guide((right, top_even), (ox + 612, top_even)) guide((right - 100, top_odd), (ox + 612, top_odd)) dimension((ox + 612, top_even), (ox + 612, top_odd), 0, T["stagger"], label_shift=-36) note(ox + 30, 610, T["note2"]) def panel_3(): oy = 640 panel_title(30, oy + 50, 3, T["title3"]) # A gauche : longueurs aleatoires, du trou rond a la lumiere entiere. slots = core.hinge_slots([rect_ring(40, oy + 100, 290, oy + 520)], length=150, width=26, bridge=30, pitch=50, random_min=26, seed=157) draw_slots(slots, 13, INK) # Cotes sur la plus courte et la plus longue lumiere de la colonne de droite. right = max(x for (x, _y), _p in slots) column = sorted((core.slot_length(s, 26), s) for s in slots if s[0][0] == right) for (_size, slot), key, shift in ((column[0], "shortest", 32), (column[-1], "longest", 52)): dimension((right + 13, slot[0][1] - 13), (right + 13, slot[1][1] + 13), -22, T[key], label_shift=shift) # A droite : le meme motif tourne. rings = [rect_ring(420, oy + 100, 660, oy + 520)] center = (540, oy + 310) shape(rings) slots = core.hinge_slots(rings, length=130, width=28, bridge=30, pitch=58, angle=30, margin=8, min_length=40) main = nearest(slots, center) draw_slots([s for s in slots if s is not main], 14, GHOST) draw_slots([main], 14, INK, 3) for a in (90, 120): ux, uy = _unit(a) polyline([center, (center[0] + 185 * ux, center[1] + 185 * uy)], DIM2, 1, 'stroke-dasharray="5,4"') arc(center, 150, 90, 120, DIM2, T["angle"], label_radius=176) note(30, oy + 610, T["note3"]) def panel_4(): ox, oy = 700, 640 panel_title(ox + 30, oy + 50, 4, T["title4"]) rings = [rect_ring(ox + 135, oy + 90, ox + 525, oy + 530), circle_ring(ox + 260, oy + 260, 55)] shape(rings) slots = core.hinge_slots(rings, length=160, width=34, bridge=36, pitch=70, margin=36, min_length=60) draw_slots(slots, 17, INK) # Marge : du bord haut au bout d'une lumiere raccourcie de la colonne centrale. cx = ox + 330 top = min((s for s in slots if abs(s[0][0] - cx) < 1), key=lambda s: s[0][1]) dimension((cx, oy + 90), (cx, top[0][1] - 17), 0, T["margin"], label_shift=36) # Longueur minimale : lumiere raccourcie en haut de la colonne de droite. right = max(x for (x, _y), _p in slots) short = min((s for s in slots if s[0][0] == right), key=lambda s: s[0][1]) dimension((right + 17, short[0][1] - 17), (right + 17, short[1][1] + 17), -(ox + 565 - right - 17), T["minimum"], label_shift=38) note(ox + 30, oy + 610, T["note4"]) def export_png(svg_path, png_path, width): exe = next((path for path in INKSCAPE if os.path.exists(path)), None) if exe is None: print("Inkscape introuvable : exporter {} a la main".format(svg_path)) return os.makedirs(os.path.dirname(png_path), exist_ok=True) subprocess.run([exe, svg_path, "--export-type=png", "--export-width={}".format(width), "--export-filename={}".format(png_path)], check=True, stderr=subprocess.DEVNULL) print(png_path) def build(language): global FS version = VERSIONS[language] out.clear() T.clear() T.update(version["text"]) FS = version["font_scale"] out.append(''.format(W, H)) for x1, y1, x2, y2 in ((700, 20, 700, H - 20), (20, 640, W - 20, 640)): out.append(''.format(x1, y1, x2, y2)) panel_1() panel_2() panel_3() panel_4() svg = ('\n' '\n{2}\n\n').format(W, H, "\n".join(out)) with open(version["svg"], "w", encoding="utf-8", newline="\n") as handle: handle.write(svg) print(version["svg"]) export_png(version["svg"], version["png"], version["png_width"]) def main(argv): languages = argv or sorted(VERSIONS) for language in languages: build(language) if __name__ == "__main__": main(sys.argv[1:])