376 lines
16 KiB
Python
376 lines
16 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 formes sont calculees par box_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 box_core # noqa: E402
|
||
|
||
W, H = 1400, 1080 # 4 panneaux de 700 x 540
|
||
|
||
INK = "#343a40" # element principal
|
||
GHOST = "#adb5bd" # elements secondaires / voisins
|
||
FILL = "#f1f3f5" # remplissage des pieces
|
||
DIM = "#d9480f" # cotes
|
||
DIM2 = "#1971c2" # cotes secondaires
|
||
EDGE = "#212529" # contour des formes
|
||
LASER = "#e03131" # trait de decoupe
|
||
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). Inkscape ne traduit pas
|
||
# le chemin d'une image : la boite de dialogue montre la version anglaise.
|
||
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": "Dimensions de la boîte",
|
||
"front": "Avant",
|
||
"lid": "Couvercle",
|
||
"length": "Longueur",
|
||
"width": "Largeur",
|
||
"height": "Hauteur",
|
||
"note1": ["Couvercle (pointillés) : « Boîte fermée ». Décochée : 5 pièces,",
|
||
"bord haut des côtés droit. Minimum : 5 × l'épaisseur."],
|
||
"title2": "Épaisseur et type de dimensions",
|
||
"bottom": "Fond",
|
||
"exterior": "Extérieure",
|
||
"interior": "Intérieure",
|
||
"thickness": "Épaisseur",
|
||
"note2": ["Extérieures : cotes hors tout. Intérieures : l'extension ajoute",
|
||
"2 épaisseurs (1 seule en hauteur si la boîte est ouverte)."],
|
||
"title3": "Largeur des créneaux",
|
||
"finger": "Créneau",
|
||
"depth": "Profondeur = épaisseur",
|
||
"note3": ["0 = 3 × l'épaisseur. Largeur ajustée pour un nombre impair",
|
||
"de créneaux par bord (ici {} sur l'avant)."],
|
||
"title4": "Compensation du kerf",
|
||
"wanted": "Créneau voulu",
|
||
"drawn": "Dessiné : créneau + kerf",
|
||
"legend_drawn": "trait dessiné",
|
||
"legend_cut": "cote après découpe",
|
||
"note4": ["Le laser enlève kerf / 2 de chaque côté du trait : le contour",
|
||
"est élargi d'autant. Défaut 0,1 mm ; 0 = pas de compensation."],
|
||
},
|
||
},
|
||
"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": "Box dimensions",
|
||
"front": "Front",
|
||
"lid": "Lid",
|
||
"length": "Length",
|
||
"width": "Width",
|
||
"height": "Height",
|
||
"note1": ["Lid (dashed): \"Closed box\". Unchecked: 5 parts, straight",
|
||
"top edge on the sides. Minimum: 5 × the thickness."],
|
||
"title2": "Thickness and dimension type",
|
||
"bottom": "Bottom",
|
||
"exterior": "Exterior",
|
||
"interior": "Interior",
|
||
"thickness": "Thickness",
|
||
"note2": ["Exterior: overall sizes. Interior: the extension adds",
|
||
"2 thicknesses (only 1 in height for an open box)."],
|
||
"title3": "Finger width",
|
||
"finger": "Finger",
|
||
"depth": "Depth = thickness",
|
||
"note3": ["0 = 3 × the thickness. The width is adjusted to get an",
|
||
"odd number of fingers per edge ({} on the front here)."],
|
||
"title4": "Kerf compensation",
|
||
"wanted": "Wanted finger",
|
||
"drawn": "Drawn: finger + kerf",
|
||
"legend_drawn": "drawn stroke",
|
||
"legend_cut": "size after cutting",
|
||
"note4": ["The laser removes kerf / 2 on each side of the stroke: the",
|
||
"outline is widened accordingly. Default 0.1 mm; 0 = none."],
|
||
},
|
||
},
|
||
}
|
||
|
||
out = []
|
||
T = {}
|
||
FS = 1.0
|
||
|
||
|
||
def fmt(v):
|
||
return "{:.2f}".format(v).rstrip("0").rstrip(".")
|
||
|
||
|
||
def pts(points):
|
||
return " ".join("{},{}".format(fmt(x), fmt(y)) for x, y in points)
|
||
|
||
|
||
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 polygon(points, stroke, width, fill="none", extra=""):
|
||
out.append('<polygon points="{}" fill="{}" stroke="{}" stroke-width="{}" '
|
||
'stroke-linejoin="miter" {}/>'.format(pts(points), fill, stroke, fmt(width), extra))
|
||
|
||
|
||
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 panel_title(x, y, number, title):
|
||
text(x, y, "{} {}".format(number, title), size=22, weight="bold", halo=False)
|
||
|
||
|
||
def place(points, x0, y0, scale):
|
||
return [(x0 + x * scale, y0 + y * scale) for x, y in points]
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# Panneaux
|
||
# --------------------------------------------------------------------------
|
||
|
||
def panel_1():
|
||
"""Longueur, largeur, hauteur : boite en perspective cavaliere."""
|
||
panel_title(30, 50, 1, T["title1"])
|
||
s = 3.2 # px par mm
|
||
length, width, height = 100, 80, 50
|
||
dx, dy = 0.5 * math.cos(math.radians(35)), -0.5 * math.sin(math.radians(35))
|
||
x0, y0 = 150, 365 # coin avant bas gauche
|
||
|
||
def p(x, y, z): # x longueur, y profondeur, z hauteur
|
||
return (x0 + (x + y * dx) * s, y0 + (y * dy - z) * s)
|
||
|
||
L, P, Z = length, width, height
|
||
# Faces visibles : avant, dessus (couvercle), droite
|
||
polygon([p(0, 0, 0), p(L, 0, 0), p(L, 0, Z), p(0, 0, Z)], EDGE, 2, FILL)
|
||
polygon([p(0, 0, Z), p(L, 0, Z), p(L, P, Z), p(0, P, Z)], EDGE, 2, "#e9ecef",
|
||
'stroke-dasharray="7,5"')
|
||
polygon([p(L, 0, 0), p(L, P, 0), p(L, P, Z), p(L, 0, Z)], EDGE, 2, "#dee2e6")
|
||
text(*p(L / 2, 0, Z / 2 - 3), T["front"], size=16, color=GHOST, anchor="middle", halo=False)
|
||
text(*p(L / 2, P / 2, Z + 2), T["lid"], size=16, color=GHOST, anchor="middle",
|
||
halo=False)
|
||
dimension(p(0, 0, 0), p(L, 0, 0), 30, T["length"])
|
||
dimension(p(L, 0, 0), p(L, P, 0), 34, T["width"], label_shift=44)
|
||
dimension(p(0, 0, 0), p(0, 0, Z), -30, T["height"], label_shift=48)
|
||
note(30, 500, T["note1"])
|
||
|
||
|
||
def panel_2():
|
||
"""Epaisseur et dimensions interieures / exterieures : coupe verticale."""
|
||
ox = 700
|
||
panel_title(ox + 30, 50, 2, T["title2"])
|
||
s = 5.0
|
||
t, width, height = 6, 80, 44 # epaisseur exageree pour la lisibilite
|
||
x0, y0 = ox + 150, 150
|
||
|
||
def rect(x, y, w, h):
|
||
polygon(place([(x, y), (x + w, y), (x + w, y + h), (x, y + h)], x0, y0, s),
|
||
EDGE, 1.6, "url(#hachures)")
|
||
|
||
rect(0, 0, t, height) # cote gauche
|
||
rect(width - t, 0, t, height) # cote droit
|
||
rect(0, height - t, width, t) # fond
|
||
text(x0 + width / 2 * s, y0 + (height - t / 2) * s + 6, T["bottom"], size=15,
|
||
color=INK, anchor="middle")
|
||
|
||
top = y0
|
||
dimension((x0, top), (x0 + width * s, top), -58, T["exterior"], label_shift=18)
|
||
dimension((x0 + t * s, top + 60), (x0 + (width - t) * s, top + 60), 0,
|
||
T["interior"], color=DIM2)
|
||
dimension((x0 + (width - t) * s, y0 + 12 * s), (x0 + width * s, y0 + 12 * s), 0,
|
||
"", color=DIM)
|
||
text(x0 + width * s + 16, y0 + 12 * s + 6, T["thickness"], size=17, color=DIM,
|
||
weight="bold")
|
||
note(ox + 30, 500, T["note2"])
|
||
|
||
|
||
def panel_3():
|
||
"""Creneaux : bord male de l'avant et bord femelle du couvercle."""
|
||
oy = 540
|
||
panel_title(30, oy + 50, 3, T["title3"])
|
||
length, t = 100, 4
|
||
finger = 3 * t # valeur par defaut (parametre a 0)
|
||
s = 5.6
|
||
x0 = 70
|
||
|
||
def edge(kind, y_top, depth):
|
||
"""Bord haut d'une piece de longueur `length`, corps de hauteur `depth` px."""
|
||
corner = 0 if kind == box_core.MALE else t
|
||
pts_ = [(x0, y_top + depth), (x0, y_top + corner * s), (x0 + t * s, y_top + corner * s)]
|
||
for a, b, level in box_core.edge_segments(length, t, finger, kind):
|
||
pts_.append((x0 + a * s, y_top + level * s))
|
||
pts_.append((x0 + b * s, y_top + level * s))
|
||
pts_ += [(x0 + (length - t) * s, y_top + corner * s),
|
||
(x0 + length * s, y_top + corner * s), (x0 + length * s, y_top + depth)]
|
||
return pts_
|
||
|
||
# Couvercle (femelle) retourne au-dessus, l'avant (male) dessous
|
||
lid_y, front_y = oy + 150, oy + 200
|
||
lid = [(x, 2 * lid_y - y) for x, y in edge(box_core.FEMALE, lid_y, 50)]
|
||
polygon(lid, GHOST, 1.4, "#f8f9fa")
|
||
polygon(edge(box_core.MALE, front_y, 150), EDGE, 2, FILL)
|
||
text(x0 + length * s + 12, lid_y - 20, T["lid"], size=15, color=GHOST)
|
||
text(x0 + length * s + 12, front_y + 60, T["front"], size=15, color=GHOST)
|
||
|
||
segs = box_core.edge_segments(length, t, finger, box_core.MALE)
|
||
a, b, _ = segs[4]
|
||
dimension((x0 + a * s, front_y), (x0 + b * s, front_y), 50, T["finger"], label_shift=20)
|
||
c, d, _ = segs[1]
|
||
mid = x0 + (c + d) / 2 * s
|
||
dimension((mid, front_y), (mid, front_y + t * s), 0, "", color=DIM2)
|
||
text(mid, front_y + t * s + 28, T["depth"], size=15, color=DIM2,
|
||
weight="bold", anchor="middle")
|
||
note(30, oy + 500, [line.format(len(segs)) for line in T["note3"]])
|
||
|
||
|
||
def panel_4():
|
||
"""Kerf : contour dessine decale de kerf / 2 vers l'exterieur."""
|
||
ox, oy = 700, 540
|
||
panel_title(ox + 30, oy + 50, 4, T["title4"])
|
||
t, finger, kerf = 4, 8, 1.2 # kerf exagere pour la lisibilite
|
||
s = 18
|
||
x0, y0 = ox + 150, oy + 160
|
||
nominal = [(0, t), (6, t), (6, 0), (6 + finger, 0), (6 + finger, t),
|
||
(20 + 2, t), (22, 13), (0, 13)]
|
||
drawn = box_core.offset_orthogonal(nominal, kerf / 2)
|
||
polygon(place(nominal, x0, y0, s), INK, 1.6, FILL, 'stroke-dasharray="6,4"')
|
||
polygon(place(drawn, x0, y0, s), LASER, 2.2)
|
||
|
||
k = kerf / 2
|
||
dimension((x0 + 6 * s, y0 + 0), (x0 + (6 + finger) * s, y0), 0, "")
|
||
text(x0 + (6 + finger / 2) * s, y0 + 36, T["wanted"], size=15, color=DIM,
|
||
anchor="middle", weight="bold")
|
||
dimension((x0 + (6 - k) * s, y0 - k * s), (x0 + (6 + finger + k) * s, y0 - k * s), -40,
|
||
T["drawn"], color=LASER, size=16)
|
||
dimension((x0 + 22 * s, y0 + 6 * s), (x0 + (22 + k) * s, y0 + 6 * s), 0, "", color=DIM2)
|
||
text(x0 + (22 + k) * s + 14, y0 + 6 * s + 6, "kerf / 2", size=16, color=DIM2,
|
||
weight="bold")
|
||
|
||
lx, ly = x0 + 10, y0 + 13 * s + 36
|
||
polyline([(lx, ly), (lx + 40, ly)], LASER, 2.2)
|
||
text(lx + 50, ly + 6, T["legend_drawn"], size=15, halo=False)
|
||
polyline([(lx + 190, ly), (lx + 230, ly)], INK, 1.6, 'stroke-dasharray="6,4"')
|
||
text(lx + 240, ly + 6, T["legend_cut"], size=15, halo=False)
|
||
note(ox + 30, oy + 500, 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(name):
|
||
global FS
|
||
version = VERSIONS[name]
|
||
out.clear()
|
||
T.clear()
|
||
T.update(version["text"])
|
||
FS = version["font_scale"]
|
||
|
||
out.append('<defs><pattern id="hachures" width="8" height="8" '
|
||
'patternUnits="userSpaceOnUse" patternTransform="rotate(45)">'
|
||
'<rect width="8" height="8" fill="{}"/>'
|
||
'<line x1="0" y1="0" x2="0" y2="8" stroke="{}" stroke-width="1.5"/>'
|
||
'</pattern></defs>'.format(FILL, GHOST))
|
||
out.append('<rect width="{}" height="{}" fill="#ffffff"/>'.format(W, H))
|
||
for x1, y1, x2, y2 in ((700, 20, 700, H - 20), (20, 540, W - 20, 540)):
|
||
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):
|
||
for name in argv or sorted(VERSIONS):
|
||
build(name)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main(sys.argv[1:])
|