#!/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 wavy_ribbon_fill_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 wavy_ribbon_fill_core as core # noqa: E402
W, H = 1400, 1280 # 4 panneaux de 700 x 640
INK = "#343a40" # element principal
GHOST = "#c9ced4" # 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": "Traits et ruban",
"spacing": "Pas",
"lines": "5 traits par ruban",
"shift": "Décalage du ruban = 6 pas",
"note1": ["Le ruban glisse sur les traits du fond et retombe dessus :",
"son décalage est un nombre entier de pas."],
"title2": "Forme de l'onde",
"height": "Hauteur d'onde",
"height_value": "7 pas",
"radius": "Rayon de virage",
"radius_value": "2,8 pas",
"stagger": "Décalage",
"stagger_2": "des traits",
"stagger_value": "1 pas",
"note2": ["Chaque trait suit la même onde (virage, droite, virage),",
"décalée d'un pas sur le côté et du décalage vers le bas."],
"title3": "Quinconce des rubans",
"columns": "Pas des colonnes = 8 pas",
"rows": "Pas des rangées",
"rows_value": "7,75 pas",
"note3": ["Une rangée sur deux penche de l'autre côté.",
"Le motif se répète toutes les deux colonnes et deux rangées."],
"title4": "Rotation et origine",
"angle": "Rotation 30°",
"origin": "Origine",
"note4": ["Origine au centre de chaque forme, ou à l'origine du document",
"pour un motif continu d'une forme à l'autre."],
},
},
"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": "Lines and ribbon",
"spacing": "Spacing",
"lines": "5 lines per ribbon",
"shift": "Ribbon shift = 6 spacings",
"note1": ["The ribbon slides over the background lines and",
"lands on them: a whole number of spacings."],
"title2": "Wave shape",
"height": "Wave height",
"height_value": "7 spacings",
"radius": "Bend radius",
"radius_value": "2.8 spacings",
"stagger": "Line",
"stagger_2": "stagger",
"stagger_value": "1 spacing",
"note2": ["Every line follows the same wave (bend, straight,",
"bend), moved one spacing sideways and down."],
"title3": "Staggered ribbons",
"columns": "Column step = 8 spacings",
"rows": "Row step",
"rows_value": "7.75 spacings",
"note3": ["Every other row leans the other way. The pattern",
"repeats every two columns and two rows."],
"title4": "Rotation and origin",
"angle": "Rotation 30°",
"origin": "Origin",
"note4": ["Origin at the centre of each shape, or at the document",
"origin for a pattern continuous across shapes."],
},
},
}
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. Le motif est
# calcule par wavy_ribbon_fill_core, puis cote avec dimension() / arc().
# --------------------------------------------------------------------------
def rect_ring(x1, y1, x2, y2):
return [(x1, y1), (x2, y1), (x2, y2), (x1, y2)]
def dot(point, color, radius=4.5):
out.append(''.format(
fmt(point[0]), fmt(point[1]), fmt(radius), color))
def guide(p, q, color):
polyline([p, q], color, 1, 'stroke-dasharray="3,3"')
def vertical_dimension(x, y1, y2, labels, color, side):
"""Cote verticale ; textes a gauche (side = -1) ou a droite (side = 1)."""
polyline([(x, y1), (x, y2)], color, 1.6)
arrow_head((x, y1), (0, -1), color)
arrow_head((x, y2), (0, 1), color)
step = 22 * FS
y = (y1 + y2) / 2.0 - (len(labels) - 1) * step / 2.0 + 6 * FS
for k, label in enumerate(labels):
text(x + side * 12, y + k * step, label, color=color,
anchor="end" if side < 0 else "start", weight="bold")
def panel_1():
panel_title(30, 50, 1, T["title1"])
s = 30.0
pattern = core.RibbonPattern(s, origin=(200.0, 315.0))
window = rect_ring(155, 110, 545, 480)
motif(pattern.fill([window]), GHOST, 2.2)
piece = (0, 0, 0, 315.0 - pattern.region_height / 2, 315.0 + pattern.region_height / 2, False)
curves = pattern.piece_curves(piece)
motif(curves[1:], INK, 2.6)
motif(curves[:1], DIM, 3.2)
dimension((170, 110), (200, 110), -22, T["spacing"], color=DIM2, label_shift=16)
dimension((380, 110), (500, 110), -22, T["lines"], label_shift=16)
dimension((200, 480), (380, 480), 22, T["shift"], label_shift=20)
note(30, 610, T["note1"])
def panel_2():
ox = 700
panel_title(ox + 30, 50, 2, T["title2"])
s = 30.0
top = 125.0
pattern = core.RibbonPattern(s, origin=(ox + 225.0, top + 165.0))
piece = (0, 0, 0, top, top + pattern.region_height, False)
curves = pattern.piece_curves(piece)
left, low = pattern.grid_x(0), top + pattern.height
radius = 2.8 * s
# Cercle du virage bas du premier trait.
out.append(''.format(fmt(left + radius), fmt(low), fmt(radius), DIM2))
motif(curves[1:], INK, 2.6)
motif(curves[:1], DIM, 3.2)
center = (left + radius, low)
ux, uy = _unit(150)
tip = (center[0] + radius * ux, center[1] + radius * uy)
polyline([center, tip], DIM2, 1.6)
arrow_head(tip, (ux, uy), DIM2)
dot(center, DIM2, 3.5)
text(center[0] + 12, center[1] + 30 * FS, T["radius"], color=DIM2, weight="bold")
text(center[0] + 12, center[1] + 52 * FS, T["radius_value"], color=DIM2, weight="bold")
# Hauteur d'onde du premier trait.
guide((left - 62, top), (pattern.grid_x(6), top), DIM)
guide((left - 62, low), (left, low), DIM)
vertical_dimension(left - 50, top, low, [T["height"], T["height_value"]], DIM, -1)
# Decalage entre les deux derniers traits.
right = pattern.grid_x(10)
y3, y4 = top + 3 * pattern.stagger, top + 4 * pattern.stagger
dot((pattern.grid_x(9), y3), DIM)
dot((right, y4), DIM)
guide((pattern.grid_x(9), y3), (right + 42, y3), DIM)
guide((right, y4), (right + 42, y4), DIM)
vertical_dimension(right + 30, y3, y4, [T["stagger"], T["stagger_2"], T["stagger_value"]],
DIM, 1)
note(ox + 30, 610, T["note2"])
def panel_3():
oy = 640
panel_title(30, oy + 50, 3, T["title3"])
s = 15.0
pattern = core.RibbonPattern(s, origin=(150.0, oy + 215.0))
window = rect_ring(60, oy + 95, 640, oy + 500)
motif(pattern.fill([window]), GHOST, 1.6)
half = pattern.region_height / 2
centers = []
for row in (0, 1):
y = oy + 215.0 + row * pattern.row_step
piece = (row, 0, row * pattern.columns, y - half, y + half, bool(row))
motif(pattern.piece_curves(piece), INK, 2.2)
centers.append((pattern.grid_x(piece[2]) + pattern.span * s / 2, y))
(x0, y0), (x1, y1) = centers
for center in centers:
dot(center, DIM)
guide((x0, y0), (x0, y1 + 100), DIM)
guide((x1, y1), (x1, y1 + 100), DIM)
guide((x0, y0), (x1 + 150, y0), DIM)
guide((x1, y1), (x1 + 150, y1), DIM)
dimension((x0, y1 + 100), (x1, y1 + 100), 0, T["columns"], label_shift=22)
vertical_dimension(x1 + 135, y0, y1, [T["rows"], T["rows_value"]], DIM, 1)
note(30, oy + 610, T["note3"])
def panel_4():
ox, oy = 700, 640
panel_title(ox + 30, oy + 50, 4, T["title4"])
center = (ox + 350.0, oy + 315.0)
shape = [circle_ring(center[0], center[1], 205)]
motif(core.fill_polylines(shape, 13.0, angle=30.0, origin=center), INK, 1.5)
polyline(shape[0] + shape[0][:1], EDGE, 2)
ux, uy = _unit(60)
guide((center[0], center[1]), (center[0], center[1] - 240), DIM2)
polyline([center, (center[0] + 240 * ux, center[1] + 240 * uy)], DIM2, 1.6)
arc(center, 225, 90, 60, DIM2)
text(center[0] + 150, center[1] - 212, T["angle"], color=DIM2, weight="bold")
dot(center, DIM, 6)
text(center[0] + 14, center[1] + 6 * FS, T["origin"], color=DIM, weight="bold")
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').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:])