#!/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 # « Diagram » de la boite de dialogue) Les motifs sont calcules par pattern_core (memes fonctions que l'extension), le schema reste donc fidele a la geometrie reelle. 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) from pattern_core import (EdgeIndex, clip_polyline, fill_pattern, # noqa: E402 motif_polylines, unit) ARM, BARB = 0.83, 0.33 # valeurs par defaut de l'extension W, H = 1400, 1080 INK = "#343a40" # motif principal GHOST = "#dde1e5" # motifs 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). 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": "Le motif Y fléché", "center": "centre", "neighbor": "centre voisin", "period": "Période P", "arm": "bras a = 83 % × P", "barb": "barbe b = 33 % × P", "stroke": "épaisseur du trait e", "note1": ["Les barbes sont parallèles aux deux autres bras ; la pointe vient", "se loger dans le « V » arrière du Y voisin. Barbe à 0 % : Y simples."], "title2": "Rotation du motif θ", "note2": ["Angle en degrés, sens anti-horaire. Le réseau entier tourne", "autour du point d'ancrage (voir 4)."], "title3": "Bord de la forme", "clip": "Découper au contour", "whole": "Motifs entiers uniquement", "margin": "marge m", "note3": ["Seuls les Y qui tiennent à m + e/2 du contour sont gardés."], "title4": "Ancrage du réseau", "document": "Origine du document", "shape": "Centre de chaque forme", "note4": ["Document : motif continu d'une forme à l'autre. Centre : chaque", "forme est centrée sur un Y (point bleu), raccord visible."], }, }, "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": "The Y arrow pattern", "center": "center", "neighbor": "neighbor", "period": "Period P", "arm": "arm a = 83 % × P", "barb": "barb b = 33 % × P", "stroke": "stroke width e", "note1": ["Barbs are parallel to the two other arms; each tip", "nests in the back “V” of the neighboring Y.", "Barb at 0 %: plain Ys."], "title2": "Pattern rotation θ", "note2": ["Angle in degrees, counterclockwise. The whole", "grid rotates around the anchor point (see 4)."], "title3": "Shape edge", "clip": "Clip at the outline", "whole": "Whole patterns only", "margin": "margin m", "note3": ["Only Ys at least m + e/2 from the outline are kept."], "title4": "Grid anchor", "document": "Document origin", "shape": "Center of each shape", "note4": ["Document: pattern continuous across shapes.", "Center: each shape is centered on a Y (blue dot)."], }, }, } 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) def clip_group(motifs, rings, color, width): index = EdgeIndex(rings) for polylines in motifs: for pl in polylines: for piece in clip_polyline(pl, index): polyline(piece, color, width) # -------------------------------------------------------------------------- # 1. Un motif : periode, bras, barbes, epaisseur # -------------------------------------------------------------------------- def panel_motif(): panel_title(30, 44, "1", T["title1"]) P = 250.0 c = (270.0, 290.0) n = (c[0] + P, c[1]) arm, barb = ARM * P, BARB * P stroke = 0.05 * P # 0,5 pour une periode de 10 # Voisins : reseau triangulaire (vecteurs a 0 et 60 degres, longueur P), # limites au cadre du panneau out.append('' ''.format(fmt(515 - 22 * FS * 2))) for i, j in ((1, 0), (0, -1), (1, -1), (0, 1), (-1, 1), (-1, 0)): a1, a2 = unit(0), unit(60) x = c[0] + (i * a1[0] + j * a2[0]) * P y = c[1] + (i * a1[1] + j * a2[1]) * P motif(motif_polylines(x, y, arm, barb), GHOST, stroke) out.append('') motif(motif_polylines(c[0], c[1], arm, barb), INK, stroke) tip = (c[0] + arm, c[1]) # Centres for point in (c, n): out.append(''.format( fmt(point[0]), fmt(point[1]), DIM2)) text(c[0] + 14, c[1] + 26 * FS, T["center"], size=15, color=DIM2, weight="bold") text(n[0] + 12, n[1] + 30 * FS, T["neighbor"], size=15, color=DIM2, weight="bold") # Periode, bras, ecart dimension(c, n, 150, T["period"], color=DIM2) dimension(c, tip, -62, T["arm"]) dimension(tip, n, -62, "P − a", color=DIM2, label_shift=40, size=15) # Barbe : du bout du bras vers l'arriere, a 120 degres b_end = (tip[0] + barb * unit(-120)[0], tip[1] + barb * unit(-120)[1]) dimension(tip, b_end, -26, T["barb"], label_shift=30, label_pos=0.3) # Angle entre deux bras arc(c, 58, 120, 240, DIM, "120°", label_radius=92) # Epaisseur du trait, en travers du bras 240 degres u = unit(240) mid = (c[0] + 0.55 * arm * u[0], c[1] + 0.55 * arm * u[1]) nrm = (-u[1], u[0]) p = (mid[0] - stroke / 2 * nrm[0], mid[1] - stroke / 2 * nrm[1]) q = (mid[0] + stroke / 2 * nrm[0], mid[1] + stroke / 2 * nrm[1]) for base, sign in ((p, -1), (q, 1)): start = (base[0] + sign * 30 * nrm[0], base[1] + sign * 30 * nrm[1]) polyline([start, base], DIM, 1.6) arrow_head(base, (-sign * nrm[0], -sign * nrm[1]), DIM, 8) text(q[0] + 36 * nrm[0] - 4, q[1] + 36 * nrm[1] + 16 * FS, T["stroke"], size=17, color=DIM, anchor="end", weight="bold") note(30, 612, T["note1"]) # -------------------------------------------------------------------------- # 2. Rotation # -------------------------------------------------------------------------- def panel_rotation(): x0 = 740 panel_title(x0, 44, "2", T["title2"]) r = 200.0 cx, cy = x0 + 310, 290.0 theta = 20.0 P = 70.0 ring = [circle_ring(cx, cy, r)] motifs = fill_pattern(ring, P, ARM, BARB, angle=theta, origin=(cx, cy)) clip_group(motifs, ring, GHOST, 3.5) motif(motif_polylines(cx, cy, ARM * P, BARB * P, theta), INK, 3.5) out.append('' .format(fmt(cx), fmt(cy), fmt(r), EDGE)) # Reference horizontale et direction du bras 0 polyline([(cx, cy), (cx + r + 40, cy)], DIM2, 1.6, 'stroke-dasharray="6,4"') text(cx + r + 44, cy + 6 * FS, "0°", size=16, color=DIM2, weight="bold") u = unit(theta) polyline([(cx, cy), (cx + (r + 40) * u[0], cy + (r + 40) * u[1])], DIM, 1.6, 'stroke-dasharray="6,4"') arc((cx, cy), 150, 0, theta, DIM, "θ", label_radius=178, size=20) note(x0, 612, T["note2"]) # -------------------------------------------------------------------------- # 3. Bord de la forme : decoupe ou motifs entiers + marge # -------------------------------------------------------------------------- def panel_edge(): y0 = 670 panel_title(30, y0, "3", T["title3"]) P = 40.0 r = 118.0 cy = y0 + 162 stroke = 3.0 # Decoupe au contour cx = 180.0 ring = [circle_ring(cx, cy, r)] clip_group(fill_pattern(ring, P, ARM, BARB, origin=(cx, cy)), ring, INK, stroke) out.append('' .format(fmt(cx), fmt(cy), fmt(r), EDGE)) text(cx, cy + r + 30 * FS, T["clip"], size=17, anchor="middle", weight="bold", halo=False) # Motifs entiers + marge cx = 510.0 margin = 14.0 ring = [circle_ring(cx, cy, r)] motifs = fill_pattern(ring, P, ARM, BARB, mode="whole", clearance=margin + stroke / 2, origin=(cx, cy)) for polylines in motifs: motif(polylines, INK, stroke) out.append('' .format(fmt(cx), fmt(cy), fmt(r), EDGE)) out.append(''.format(fmt(cx), fmt(cy), fmt(r - margin), DIM)) u = unit(35) dimension((cx + (r - margin) * u[0], cy + (r - margin) * u[1]), (cx + r * u[0], cy + r * u[1]), 0, "", color=DIM) text(cx + (r + 12) * u[0], cy + (r + 12) * u[1] - 4, T["margin"], size=17, color=DIM, weight="bold") text(cx, cy + r + 30 * FS, T["whole"], size=17, anchor="middle", weight="bold", halo=False) note(30, H - 28, T["note3"]) # -------------------------------------------------------------------------- # 4. Ancrage du reseau # -------------------------------------------------------------------------- def panel_anchor(): x0, y0 = 740, 670 panel_title(x0, y0, "4", T["title4"]) P = 40.0 stroke = 3.0 w, h = 150.0, 220.0 top = y0 + 45 def pair(left, anchor_shape): shapes = [[[(left, top), (left + w, top), (left + w, top + h), (left, top + h)]], [[(left + w, top), (left + 2 * w, top), (left + 2 * w, top + h), (left + w, top + h)]]] for ring in shapes: (xa, ya), _, (xb, yb), _ = ring[0] origin = ((xa + xb) / 2, (ya + yb) / 2) if anchor_shape else (x0, y0) clip_group(fill_pattern(ring, P, ARM, BARB, origin=origin), ring, INK, stroke) out.append(''.format( fmt(xa), fmt(ya), fmt(w), fmt(h), EDGE)) if anchor_shape: out.append(''.format( fmt(origin[0]), fmt(origin[1]), DIM2)) pair(x0 + 15, False) pair(x0 + 335, True) text(x0 + 15 + w, top + h + 30 * FS, T["document"], size=17, anchor="middle", weight="bold", halo=False) text(x0 + 335 + w, top + h + 30 * FS, T["shape"], size=17, anchor="middle", weight="bold", halo=False) note(x0, H - 28, 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_motif() panel_rotation() panel_edge() panel_anchor() 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:])