425 lines
17 KiB
Python
425 lines
17 KiB
Python
#!/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('<polyline points="{}" fill="none" stroke="{}" stroke-width="{}" '
|
||
'stroke-linejoin="miter" {}/>'.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('<text x="{}" y="{}" text-anchor="{}" style="{}">{}</text>'.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('<polygon points="{}" fill="{}"/>'.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('<clipPath id="panel1"><rect x="0" y="62" width="698" height="{}"/>'
|
||
'</clipPath><g clip-path="url(#panel1)">'.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('</g>')
|
||
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('<circle cx="{}" cy="{}" r="5" fill="{}"/>'.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('<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{}" stroke-width="2"/>'
|
||
.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('<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{}" stroke-width="2"/>'
|
||
.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('<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{}" stroke-width="2"/>'
|
||
.format(fmt(cx), fmt(cy), fmt(r), EDGE))
|
||
out.append('<circle cx="{}" cy="{}" r="{}" fill="none" stroke="{}" stroke-width="1.4" '
|
||
'stroke-dasharray="5,4"/>'.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('<rect x="{}" y="{}" width="{}" height="{}" fill="none" '
|
||
'stroke="{}" stroke-width="2"/>'.format(
|
||
fmt(xa), fmt(ya), fmt(w), fmt(h), EDGE))
|
||
if anchor_shape:
|
||
out.append('<circle cx="{}" cy="{}" r="5" fill="{}"/>'.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('<rect width="{}" height="{}" fill="#ffffff"/>'.format(W, H))
|
||
for x1, y1, x2, y2 in ((700, 20, 700, H - 20), (20, 640, W - 20, 640)):
|
||
out.append('<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="#dee2e6" '
|
||
'stroke-width="1.5"/>'.format(x1, y1, x2, y2))
|
||
panel_motif()
|
||
panel_rotation()
|
||
panel_edge()
|
||
panel_anchor()
|
||
svg = ('<?xml version="1.0" encoding="UTF-8"?>\n'
|
||
'<svg xmlns="http://www.w3.org/2000/svg" width="{0}" height="{1}" '
|
||
'viewBox="0 0 {0} {1}">\n{2}\n</svg>\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:])
|