#!/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').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:])