336 lines
14 KiB
Python
336 lines
14 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 gray_iso_layers_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 numpy as np # noqa: E402
|
|
|
|
import gray_iso_layers_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, repere d'assemblage
|
|
CUT = "#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).
|
|
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": "Image de départ",
|
|
"note1": ["Image bitmap sélectionnée, lue en nuances de gris",
|
|
"(couleurs converties, transparence = blanc)."],
|
|
"title2": "Lissage",
|
|
"low": "Lissage 0,5 %",
|
|
"high": "Lissage 4 %",
|
|
"note2": ["Lissage appliqué avant la découpe, en % du grand côté :",
|
|
"plus il est fort, plus les contours sont ronds."],
|
|
"title3": "Nombre de niveaux de gris",
|
|
"levels": "5 niveaux",
|
|
"note3": ["Seuils régulièrement espacés du plus sombre au plus",
|
|
"clair. Chaque aplat est bordé d'un contour fermé."],
|
|
"title4": "Une planche par niveau, à empiler",
|
|
"level": "Planche n° {}",
|
|
"all": "Planches empilées",
|
|
"note4": ["Un calque par planche. Rouge : découpe de la planche.",
|
|
"Noir : repère de la planche suivante, à coller dessus."],
|
|
},
|
|
},
|
|
"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": "Source image",
|
|
"note1": ["Selected bitmap image, read as shades of gray",
|
|
"(colors converted, transparency = white)."],
|
|
"title2": "Smoothing",
|
|
"low": "Smoothing 0.5 %",
|
|
"high": "Smoothing 4 %",
|
|
"note2": ["Smoothing applied before cutting, in % of the longer",
|
|
"side: the higher, the rounder the outlines."],
|
|
"title3": "Number of gray levels",
|
|
"levels": "5 levels",
|
|
"note3": ["Evenly spaced thresholds from darkest to lightest.",
|
|
"Each flat area gets a closed outline."],
|
|
"title4": "One board per level, to stack",
|
|
"level": "Board no. {}",
|
|
"all": "Stacked boards",
|
|
"note4": ["One layer per board. Red: cut outline of the board.",
|
|
"Black: marking of the next board, glued on top."],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
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 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)
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Relief d'exemple et dessin des couches avec les fonctions du noyau
|
|
# --------------------------------------------------------------------------
|
|
|
|
COLS, ROWS = 161, 121 # echantillons du relief d'exemple (4:3)
|
|
|
|
|
|
def sample_field():
|
|
"""Relief synthetique : quelques bosses larges et un grain fin, pour que
|
|
l'effet du lissage se voie."""
|
|
rng = np.random.default_rng(7)
|
|
y, x = np.mgrid[0:ROWS, 0:COLS]
|
|
field = np.zeros((ROWS, COLS))
|
|
for _ in range(9):
|
|
cx, cy = rng.uniform(0, COLS), rng.uniform(0, ROWS)
|
|
radius = rng.uniform(18, 42)
|
|
field += rng.uniform(0.5, 1.0) * np.exp(
|
|
-((x - cx) ** 2 + (y - cy) ** 2) / (2 * radius ** 2))
|
|
grain = core.gaussian_blur(rng.random((ROWS, COLS)), 1.6)
|
|
return core.normalize(field) + 1.5 * (grain - grain.mean())
|
|
|
|
|
|
FIELD = sample_field()
|
|
|
|
|
|
def draw_layers(layers, x, y, width, height, stroke=EDGE, stroke_width=1.2,
|
|
fill=True, transform=None):
|
|
"""Dessine des couches du noyau dans le rectangle (x, y, width, height)."""
|
|
scale_x, scale_y = width / (COLS - 1.0), height / (ROWS - 1.0)
|
|
box = (x, y, x + width, y + height)
|
|
for _level, gray, rings in layers:
|
|
if not rings:
|
|
continue
|
|
d = core.rings_to_d(core.scale_rings(rings, scale_x, scale_y, x, y),
|
|
box=box, precision=1)
|
|
extra = ""
|
|
if transform:
|
|
# Vue en perspective : le trait garde son epaisseur a l'ecran.
|
|
extra = ' transform="{}" vector-effect="non-scaling-stroke"'.format(transform)
|
|
out.append('<path d="{}" fill="{}" fill-rule="evenodd" stroke="{}" '
|
|
'stroke-width="{}" stroke-linejoin="round"{}/>'.format(
|
|
d, core.gray_to_hex(gray) if fill else "none",
|
|
stroke, fmt(stroke_width), extra))
|
|
|
|
|
|
def layers_for(levels, blur_percent, shapes="band"):
|
|
return core.iso_layers(FIELD, levels=levels, blur=blur_percent / 100.0 * COLS,
|
|
min_area=0.0005, tolerance=0.3, shapes=shapes,
|
|
preserve_edges=True)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Panneaux
|
|
# --------------------------------------------------------------------------
|
|
|
|
def panel_1():
|
|
panel_title(30, 50, 1, T["title1"])
|
|
# Beaucoup de niveaux sans trait, empiles pour ne laisser aucun joint :
|
|
# rendu continu, comme le bitmap.
|
|
draw_layers(layers_for(40, 0.0, "light"), 110, 110, 480, 360, stroke="none")
|
|
out.append('<rect x="110" y="110" width="480" height="360" fill="none" '
|
|
'stroke="{}" stroke-width="1.2"/>'.format(EDGE))
|
|
note(30, 610, T["note1"])
|
|
|
|
|
|
def panel_2():
|
|
panel_title(730, 50, 2, T["title2"])
|
|
for x, blur, label in ((730, 0.5, T["low"]), (1060, 4.0, T["high"])):
|
|
draw_layers(layers_for(5, blur), x, 150, 310, 232.5)
|
|
text(x + 155, 420, label, color=DIM, anchor="middle", weight="bold",
|
|
halo=False)
|
|
note(730, 610, T["note2"])
|
|
|
|
|
|
def panel_3():
|
|
panel_title(30, 690, 3, T["title3"])
|
|
draw_layers(layers_for(5, 2.0), 30, 750, 440, 330)
|
|
# Echelle des 5 gris, du plus clair en haut au plus sombre en bas.
|
|
for k in range(5):
|
|
out.append('<rect x="500" y="{}" width="40" height="66" fill="{}" '
|
|
'stroke="{}" stroke-width="1.2"/>'.format(
|
|
750 + 66 * k, core.gray_to_hex((4 - k) / 4.0), EDGE))
|
|
dimension((540, 750), (540, 1080), -24, "", label_shift=0)
|
|
text(578, 921, T["levels"], color=DIM, weight="bold",
|
|
halo=False)
|
|
note(30, 1250, T["note3"])
|
|
|
|
|
|
def panel_4():
|
|
panel_title(730, 690, 4, T["title4"])
|
|
# Une planche par vignette, comme dans son calque : sa decoupe en rouge
|
|
# et le repere de la planche suivante en noir ; puis la pile entiere.
|
|
boards = core.iso_boards(FIELD, levels=5, blur=2.0 / 100.0 * COLS,
|
|
min_area=0.0005, tolerance=0.3, shapes="light",
|
|
preserve_edges=True)
|
|
slots = [(730 + 220 * (k % 3), 745 + 215 * (k // 3)) for k in range(6)]
|
|
scale_x, scale_y = 200 / (COLS - 1.0), 150 / (ROWS - 1.0)
|
|
for k, (x, y) in enumerate(slots):
|
|
if k == 5:
|
|
draw_layers([board[:3] for board in boards], x, y, 200, 150,
|
|
stroke_width=1)
|
|
text(x + 100, y + 150 + 22 * FS, T["all"], size=15, anchor="middle",
|
|
color=DIM, weight="bold", halo=False)
|
|
continue
|
|
_level, _gray, rings, mark = boards[k]
|
|
box = (x, y, x + 200, y + 150)
|
|
cut = core.rings_to_d(core.scale_rings(rings, scale_x, scale_y, x, y),
|
|
box=box, precision=1)
|
|
out.append('<path d="{}" fill="#fff5f5" fill-rule="evenodd" stroke="{}" '
|
|
'stroke-width="1.6" stroke-linejoin="round"/>'.format(cut, CUT))
|
|
if mark:
|
|
line = core.lines_to_d(core.scale_lines(mark, scale_x, scale_y, x, y),
|
|
box=box, precision=1)
|
|
out.append('<path d="{}" fill="none" stroke="{}" stroke-width="1.3" '
|
|
'stroke-linejoin="round"/>'.format(line, EDGE))
|
|
text(x + 100, y + 150 + 22 * FS, T["level"].format(k + 1), size=15,
|
|
anchor="middle", halo=False)
|
|
note(730, 1250, 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:])
|