inkscape.BoxFingering/docs/schema_parametres.py
2026-09-24 18:42:22 +02:00

376 lines
16 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 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:])