#!/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(''.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('{}'.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(''.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(''.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(''.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(''.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(''.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(''.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(''.format(W, H)) for x1, y1, x2, y2 in ((700, 20, 700, H - 20), (20, 640, W - 20, 640)): out.append(''.format(x1, y1, x2, y2)) panel_1() panel_2() panel_3() panel_4() svg = ('\n' '\n{2}\n\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:])