inkscape.Ypatern/docs/schema_parametres.py
2026-09-24 10:39:18 +02:00

425 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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