#!/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 voronoi_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 voronoi_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": "Taille des cellules", "size": "taille", "note1": ["Distance moyenne entre deux germes voisins ; chaque cellule", "regroupe les points plus proches de son germe que des autres."], "title2": "Largeur du filet", "width": "largeur", "note2": ["Écart constant entre deux cellules voisines. Le filet", "produit est la forme percée par les cellules."], "title3": "Filet sur le contour", "with": "coché", "without": "décoché", "frame": "largeur", "note3": ["Coché : un cadre de la largeur du filet borde la forme.", "Décoché : les cellules touchent le bord."], "title4": "Disposition et arrondi", "poisson": ["Aléatoire", "homogène"], "random": ["Aléatoire", "pur"], "hexagonal": ["Hexagonale", "irrégularité 0 %"], "rounded": ["Arrondi", "60 %"], "note4": ["La graine aléatoire donne un autre motif", "avec les mêmes réglages."], }, }, "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": "Cell size", "size": "size", "note1": ["Average distance between neighbouring seeds;", "a cell holds the points closest to its seed."], "title2": "Net width", "width": "width", "note2": ["Constant gap between neighbouring cells.", "The net is the shape pierced by the cells."], "title3": "Net along the outline", "with": "checked", "without": "unchecked", "frame": "width", "note3": ["Checked: a frame as wide as the net.", "Unchecked: cells touch the edge."], "title4": "Layout and roundness", "poisson": ["Even", "random"], "random": ["Pure", "random"], "hexagonal": ["Hexagonal", "0 %"], "rounded": ["Roundness", "60 %"], "note4": ["The random seed gives another", "pattern with the same settings."], }, }, } 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 parametre (ou groupe de parametres), numerotes 1 a 4. # Les cellules sont calculees par voronoi_core, en pixels du schema. # -------------------------------------------------------------------------- NET = "#495057" # filet CELL = "#ffffff" # cellules def region(rings, fill, stroke="none", width=0): out.append(''.format( voronoi_core.rings_to_d(rings, 2), fill, stroke, fmt(width))) def dot(p, r=4.5, color=INK): out.append(''.format( fmt(p[0]), fmt(p[1]), fmt(r), color)) def filled(shape, cell_size, net_width, **options): """Filet plein + contour de la forme, comme le produit l'extension.""" cells = voronoi_core.fill_shape(shape, cell_size, net_width, **options) region(voronoi_core.net_rings(shape, cells), NET) region(shape, "none", EDGE, 1.5) return cells def nearest_pair(points, center): """Deux germes voisins (les plus proches l'un de l'autre) pres du centre.""" best = None for k, p in enumerate(points): if math.hypot(p[0] - center[0], p[1] - center[1]) > 120: continue for q in points[k + 1:]: d = math.hypot(p[0] - q[0], p[1] - q[1]) if best is None or d < best[0]: best = (d, p, q) return best[1], best[2] def panel_1(): panel_title(30, 50, 1, T["title1"]) box = [(60, 90), (640, 90), (640, 520), (60, 520)] points = voronoi_core.make_points("poisson", (0, 40, 700, 570), 95, seed=3) cells = voronoi_core.voronoi_cells(points, (-100, -60, 800, 670)) for cell in cells: piece = voronoi_core.clip_convex([box], cell) if piece: region(piece, "none", "#adb5bd", 1.4) region([box], "none", EDGE, 1.5) for p in points: if 60 < p[0] < 640 and 90 < p[1] < 520: dot(p) p, q = nearest_pair(points, (350, 300)) dimension(p, q, 0, T["size"], label_shift=20) note(30, 610, T["note1"]) def panel_2(): panel_title(730, 50, 2, T["title2"]) box = [(760, 90), (1340, 90), (1340, 520), (760, 520)] shape = [box] net = 24 filled(shape, 150, net, distribution="hexagonal", irregularity=45, seed=2) # Cote a travers le brin qui separe deux cellules voisines. points = voronoi_core.make_points("hexagonal", (760 - 150, 90 - 150, 1340 + 150, 520 + 150), 150, 45, 2) p, q = nearest_pair(points, (1050, 300)) ux, uy = (q[0] - p[0]) / math.hypot(q[0] - p[0], q[1] - p[1]), \ (q[1] - p[1]) / math.hypot(q[0] - p[0], q[1] - p[1]) m = ((p[0] + q[0]) / 2, (p[1] + q[1]) / 2) a = (m[0] - ux * (net / 2 + 34), m[1] - uy * (net / 2 + 34)) b = (m[0] - ux * net / 2, m[1] - uy * net / 2) c = (m[0] + ux * net / 2, m[1] + uy * net / 2) d = (m[0] + ux * (net / 2 + 34), m[1] + uy * (net / 2 + 34)) polyline([a, b], DIM, 1.8) polyline([d, c], DIM, 1.8) arrow_head(b, (ux, uy), DIM) arrow_head(c, (-ux, -uy), DIM) text(d[0] + 12, d[1] + 6, T["width"], size=17, color=DIM, weight="bold") note(730, 610, T["note2"]) def panel_3(): panel_title(30, 690, 3, T["title3"]) net = 14 for cx, border, label in ((180, True, T["with"]), (520, False, T["without"])): shape = [circle_ring(cx, 925, 152, 240)] filled(shape, 66, net, seed=4, border=border) text(cx, 1135, label, size=18, anchor="middle", weight="bold", halo=False) # Cote du cadre, sur le cercle de gauche dimension((180 + 152 - net, 925), (180 + 152, 925), -167, T["frame"], color=DIM2, label_shift=22) note(30, 1250, T["note3"]) def panel_4(): panel_title(730, 690, 4, T["title4"]) tiles = (("poisson", 0, T["poisson"]), ("random", 0, T["random"]), ("hexagonal", 0, T["hexagonal"]), ("poisson", 0.6, T["rounded"])) for k, (distribution, roundness, label) in enumerate(tiles): x = 745 + k * 160 box = [(x, 740), (x + 140, 740), (x + 140, 1060), (x, 1060)] filled([box], 40, 5, distribution=distribution, irregularity=0, seed=6, roundness=roundness) for m, line in enumerate(label): text(x + 70, 1092 + m * 21 * FS, line, size=15, anchor="middle", halo=False) note(730, 1250, 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:])