inkscape.BoxFingering/docs/schema_parametres.py
2026-09-24 17:35:32 +02:00

319 lines
13 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 readme # docs/parametres.svg + .png (README)
python docs/schema_parametres.py dialogue # images/parametres.png (onglet
# « Schéma » 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"]
# L'extension n'existe qu'en francais : deux versions du meme schema.
# La version « dialogue » est affichee reduite (900 px de large) dans la boite de
# dialogue : textes x 1.25 pour rester lisibles.
VERSIONS = {
"readme": {
"svg": os.path.join(HERE, "parametres.svg"),
"png": os.path.join(HERE, "parametres.png"),
"png_width": 1400,
"font_scale": 1.0,
},
"dialogue": {
"svg": os.path.join(HERE, "parametres_dialogue.svg"),
"png": os.path.join(ROOT, "images", "parametres.png"),
"png_width": 900,
"font_scale": 1.25,
},
}
out = []
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, "Dimensions de la boîte")
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), "Avant", size=16, color=GHOST, anchor="middle", halo=False)
text(*p(L / 2, P / 2, Z + 2), "Couvercle", size=16, color=GHOST, anchor="middle",
halo=False)
dimension(p(0, 0, 0), p(L, 0, 0), 30, "Longueur")
dimension(p(L, 0, 0), p(L, P, 0), 34, "Largeur", label_shift=44)
dimension(p(0, 0, 0), p(0, 0, Z), -30, "Hauteur", label_shift=48)
note(30, 500, ["Couvercle (pointillés) : « Boîte fermée ». Décochée : 5 pièces,",
"bord haut des côtés droit. Minimum : 5 × l'épaisseur."])
def panel_2():
"""Epaisseur et dimensions interieures / exterieures : coupe verticale."""
ox = 700
panel_title(ox + 30, 50, 2, "Épaisseur et type de dimensions")
s = 5.0
t, width, height = 6, 80, 44 # epaisseur exageree pour la lisibilite
x0, y0 = ox + 150, 150
hatch = 'fill="url(#hachures)"'
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
del hatch
text(x0 + width / 2 * s, y0 + (height - t / 2) * s + 6, "Fond", size=15,
color=INK, anchor="middle")
top = y0
dimension((x0, top), (x0 + width * s, top), -58, "Extérieure", label_shift=18)
dimension((x0 + t * s, top + 60), (x0 + (width - t) * s, top + 60), 0,
"Intérieure", 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, "Épaisseur", size=17, color=DIM,
weight="bold")
note(ox + 30, 500, ["Extérieures : cotes hors tout. Intérieures : l'extension ajoute",
"2 épaisseurs (1 seule en hauteur si la boîte est ouverte)."])
def panel_3():
"""Creneaux : bord male de l'avant et bord femelle du couvercle."""
oy = 540
panel_title(30, oy + 50, 3, "Largeur des créneaux")
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, "Couvercle", size=15, color=GHOST)
text(x0 + length * s + 12, front_y + 60, "Avant", 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, "Créneau", 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, "Profondeur = épaisseur", size=15, color=DIM2,
weight="bold", anchor="middle")
note(30, oy + 500, ["0 = 3 × l'épaisseur. Largeur ajustée pour un nombre impair",
"de créneaux par bord (ici {} sur l'avant).".format(len(segs))])
def panel_4():
"""Kerf : contour dessine decale de kerf / 2 vers l'exterieur."""
ox, oy = 700, 540
panel_title(ox + 30, oy + 50, 4, "Compensation du kerf")
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, "Créneau voulu", 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,
"Dessiné : créneau + kerf", 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, "trait dessiné", size=15, halo=False)
polyline([(lx + 190, ly), (lx + 230, ly)], INK, 1.6, 'stroke-dasharray="6,4"')
text(lx + 240, ly + 6, "cote après découpe", size=15, halo=False)
note(ox + 30, oy + 500, ["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."])
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()
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:])