380 lines
15 KiB
Python
380 lines
15 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
|
|
# « Help » de la boite de dialogue)
|
|
|
|
Les formes sont calculees par living_hinge_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 living_hinge_core as 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": "Lumière : longueur et largeur",
|
|
"note1": ["Chaque lumière est un rectangle à bouts arrondis ;",
|
|
"la longueur se mesure hors tout."],
|
|
"length": "longueur",
|
|
"width": "largeur",
|
|
"title2": "Pont, pas et décalage",
|
|
"note2": ["Pont : matière entre deux lumières d'une colonne. Pas : entraxe",
|
|
"des colonnes. Décalage en % de (longueur + pont), 50 % = quinconce."],
|
|
"bridge": "pont",
|
|
"pitch": "pas",
|
|
"title3": "Longueurs aléatoires et angle",
|
|
"note3": ["Aléatoire : longueurs au hasard entre le mini et la longueur,",
|
|
"colonnes pleines d'une marge à l'autre. L'angle tourne le motif."],
|
|
"shortest": "mini",
|
|
"longest": "longueur",
|
|
"stagger": "50 %",
|
|
"angle": "angle",
|
|
"title4": "Marge et longueur minimale",
|
|
"note4": ["Près du bord et des trous, les lumières sont raccourcies à",
|
|
"la marge ; plus courtes que le minimum, elles disparaissent."],
|
|
"margin": "marge",
|
|
"minimum": "≥ mini",
|
|
},
|
|
},
|
|
"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": "Slot: length and width",
|
|
"note1": ["Each slot is a rectangle with rounded ends;",
|
|
"its length is measured end to end."],
|
|
"length": "length",
|
|
"width": "width",
|
|
"title2": "Bridge, pitch and stagger",
|
|
"note2": ["Bridge: material between two slots of a column. Pitch: column",
|
|
"spacing. Stagger in % of (length + bridge), 50 % = staggered."],
|
|
"bridge": "bridge",
|
|
"pitch": "pitch",
|
|
"title3": "Random lengths and angle",
|
|
"note3": ["Random: lengths drawn between the shortest and the length,",
|
|
"columns filled from margin to margin. The angle turns the pattern."],
|
|
"shortest": "min",
|
|
"longest": "length",
|
|
"stagger": "50 %",
|
|
"angle": "angle",
|
|
"title4": "Margin and minimum length",
|
|
"note4": ["Near the edge and holes, slots are shortened to the",
|
|
"margin; shorter than the minimum, they are dropped."],
|
|
"margin": "margin",
|
|
"minimum": "≥ min",
|
|
},
|
|
},
|
|
}
|
|
|
|
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('<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)
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Panneaux : un par groupe de parametres, numerotes 1 a 4. Les lumieres sont
|
|
# calculees par le noyau (hinge_slots, slot_outline), en pixels du schema.
|
|
# --------------------------------------------------------------------------
|
|
|
|
def rect_ring(x0, y0, x1, y1):
|
|
return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
|
|
|
|
|
def shape(rings, fill="#f1f3f5"):
|
|
d = " ".join("M {} Z".format(" L ".join("{},{}".format(fmt(x), fmt(y)) for x, y in ring))
|
|
for ring in rings)
|
|
out.append('<path d="{}" fill="{}" fill-rule="evenodd" stroke="{}" '
|
|
'stroke-width="2"/>'.format(d, fill, EDGE))
|
|
|
|
|
|
def draw_slots(slots, radius, color, width=2.5):
|
|
for slot in slots:
|
|
out.append('<polygon points="{}" fill="#ffffff" stroke="{}" stroke-width="{}" '
|
|
'stroke-linejoin="round"/>'.format(
|
|
pts(core.slot_outline(slot, radius)), color, fmt(width)))
|
|
|
|
|
|
def guide(p, q, color=DIM):
|
|
polyline([p, q], color, 1, 'stroke-dasharray="3,3"')
|
|
|
|
|
|
def nearest(slots, point):
|
|
"""Lumiere dont le milieu est le plus proche de `point`."""
|
|
return min(slots, key=lambda s: math.hypot((s[0][0] + s[1][0]) / 2 - point[0],
|
|
(s[0][1] + s[1][1]) / 2 - point[1]))
|
|
|
|
|
|
def panel_1():
|
|
panel_title(30, 50, 1, T["title1"])
|
|
slots = core.hinge_slots([rect_ring(140, 100, 560, 540)], length=300, width=60,
|
|
bridge=50, pitch=150, min_length=60)
|
|
main = nearest(slots, (350, 320))
|
|
draw_slots([s for s in slots if s is not main], 30, GHOST)
|
|
draw_slots([main], 30, INK, 3)
|
|
(x, y0), (_x, y1) = main
|
|
dimension((x - 30, y0 - 30), (x - 30, y1 + 30), 45, T["length"], label_shift=48)
|
|
dimension((x - 30, y0), (x + 30, y0), -75, T["width"])
|
|
note(30, 610, T["note1"])
|
|
|
|
|
|
def panel_2():
|
|
ox = 700
|
|
panel_title(ox + 30, 50, 2, T["title2"])
|
|
slots = core.hinge_slots([rect_ring(ox + 90, 90, ox + 590, 500)], length=200,
|
|
width=44, bridge=50, pitch=100, min_length=44)
|
|
draw_slots(slots, 22, INK)
|
|
# Pont : entre la lumiere centrale de la colonne de gauche et celle du dessous.
|
|
left = min(x for (x, _y), _p in slots)
|
|
column = sorted(s for s in slots if s[0][0] == left)
|
|
upper = nearest(column, (left, 295))
|
|
lower = column[column.index(upper) + 1]
|
|
dimension((left - 22, upper[1][1] + 22), (left - 22, lower[0][1] - 22), 30,
|
|
T["bridge"], label_shift=34)
|
|
# Pas : entre les axes de deux colonnes voisines, sous le motif.
|
|
dimension((ox + 340, 500), (ox + 440, 500), 25, T["pitch"])
|
|
# Decalage : entre le haut d'une lumiere paire et celui de sa voisine impaire.
|
|
right = max(x for (x, _y), _p in slots)
|
|
even = nearest([s for s in slots if s[0][0] == right], (right, 295))
|
|
odd = nearest([s for s in slots if s[0][0] == right - 100], (right - 100, 420))
|
|
top_even, top_odd = even[0][1] - 22, odd[0][1] - 22
|
|
guide((right, top_even), (ox + 612, top_even))
|
|
guide((right - 100, top_odd), (ox + 612, top_odd))
|
|
dimension((ox + 612, top_even), (ox + 612, top_odd), 0, T["stagger"], label_shift=-36)
|
|
note(ox + 30, 610, T["note2"])
|
|
|
|
|
|
def panel_3():
|
|
oy = 640
|
|
panel_title(30, oy + 50, 3, T["title3"])
|
|
# A gauche : longueurs aleatoires, du trou rond a la lumiere entiere.
|
|
slots = core.hinge_slots([rect_ring(40, oy + 100, 290, oy + 520)], length=150,
|
|
width=26, bridge=30, pitch=50, random_min=26, seed=157)
|
|
draw_slots(slots, 13, INK)
|
|
# Cotes sur la plus courte et la plus longue lumiere de la colonne de droite.
|
|
right = max(x for (x, _y), _p in slots)
|
|
column = sorted((core.slot_length(s, 26), s) for s in slots if s[0][0] == right)
|
|
for (_size, slot), key, shift in ((column[0], "shortest", 32), (column[-1], "longest", 52)):
|
|
dimension((right + 13, slot[0][1] - 13), (right + 13, slot[1][1] + 13), -22,
|
|
T[key], label_shift=shift)
|
|
|
|
# A droite : le meme motif tourne.
|
|
rings = [rect_ring(420, oy + 100, 660, oy + 520)]
|
|
center = (540, oy + 310)
|
|
shape(rings)
|
|
slots = core.hinge_slots(rings, length=130, width=28, bridge=30, pitch=58,
|
|
angle=30, margin=8, min_length=40)
|
|
main = nearest(slots, center)
|
|
draw_slots([s for s in slots if s is not main], 14, GHOST)
|
|
draw_slots([main], 14, INK, 3)
|
|
for a in (90, 120):
|
|
ux, uy = _unit(a)
|
|
polyline([center, (center[0] + 185 * ux, center[1] + 185 * uy)], DIM2, 1,
|
|
'stroke-dasharray="5,4"')
|
|
arc(center, 150, 90, 120, DIM2, T["angle"], label_radius=176)
|
|
note(30, oy + 610, T["note3"])
|
|
|
|
|
|
def panel_4():
|
|
ox, oy = 700, 640
|
|
panel_title(ox + 30, oy + 50, 4, T["title4"])
|
|
rings = [rect_ring(ox + 135, oy + 90, ox + 525, oy + 530),
|
|
circle_ring(ox + 260, oy + 260, 55)]
|
|
shape(rings)
|
|
slots = core.hinge_slots(rings, length=160, width=34, bridge=36, pitch=70,
|
|
margin=36, min_length=60)
|
|
draw_slots(slots, 17, INK)
|
|
# Marge : du bord haut au bout d'une lumiere raccourcie de la colonne centrale.
|
|
cx = ox + 330
|
|
top = min((s for s in slots if abs(s[0][0] - cx) < 1), key=lambda s: s[0][1])
|
|
dimension((cx, oy + 90), (cx, top[0][1] - 17), 0, T["margin"], label_shift=36)
|
|
# Longueur minimale : lumiere raccourcie en haut de la colonne de droite.
|
|
right = max(x for (x, _y), _p in slots)
|
|
short = min((s for s in slots if s[0][0] == right), key=lambda s: s[0][1])
|
|
dimension((right + 17, short[0][1] - 17), (right + 17, short[1][1] + 17),
|
|
-(ox + 565 - right - 17), T["minimum"], label_shift=38)
|
|
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('<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_1()
|
|
panel_2()
|
|
panel_3()
|
|
panel_4()
|
|
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:])
|