#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ patch_render_engine_c3b.py — Chantier C3b (centrage vertical) ============================================================= Corrige deux défauts de centrage vertical révélés par les previews C2, SANS toucher au helper _cy() (le centrage du contenu dans la zone est un choix de design assumé — le contenu respire). P1. big_stat : la hauteur de bloc était codée en dur (block = 8.13), sous-estimée par rapport au placement réel (chiffre à y, desc à y+5.33, source à y+7.37 ≈ 8.3+). Résultat : _cy() centrait un bloc faussé, l'ensemble penchait vers le bas. Correctif : hauteur réelle calculée (avec/sans source) → centrage exact. Les offsets internes deviennent relatifs à cette hauteur. P2. executive_summary : dans chaque carte (hauteur ch=3.68), le label et le texte étaient posés à des offsets FIXES (y+0.56, y+1.57), collés en haut, laissant du vide en bas de carte. Correctif : le bloc label+texte est centré verticalement dans la carte (offsets dérivés de ch), à côté du badge déjà centré. Aucune dépendance nouvelle ; réutilise measure via estimate_text_height déjà présent. Vérifie chaque ancre, écrit .bak-c3b, compile, idempotent. Usage (dossier du pipeline, single-line) : python3 patch_render_engine_c3b.py """ import py_compile import shutil import sys from pathlib import Path TARGET = Path("render_engine_v2.py") PATCHES = [ # P1 — big_stat : hauteur réelle ( ' def _render_big_stat(self, slide, d):\n' ' self._title(slide, pick(d, "titre", "title"))\n' ' val = pick(d, "valeur", "stat", "chiffre", "value")\n' ' desc = pick(d, "description", "texte", "label")\n' ' src = pick(d, "source", "reference")\n' ' block = 8.13\n' ' y = self._cy(block)\n' ' self._text(slide, 0, y, self.SLIDE_W, 5.1, val,\n' ' font=self.F_DISPLAY, size=self.T["stat_hero"], ' 'bold=True,\n' ' color=self.C["coral"], align=PP_ALIGN.CENTER,\n' ' anchor=MSO_ANCHOR.MIDDLE)\n' ' self._text(slide, 6.86, y + 5.33, self.SLIDE_W - 13.72, ' '2.0, desc,\n' ' size=18, color=self.C["body"], ' 'align=PP_ALIGN.CENTER)\n' ' if src:\n' ' self._text(slide, 6.86, y + 7.37, self.SLIDE_W - ' '13.72, 0.9, src,\n' ' size=11, italic=True, ' 'color=self.C["muted"],\n' ' align=PP_ALIGN.CENTER)\n', ' def _render_big_stat(self, slide, d):\n' ' self._title(slide, pick(d, "titre", "title"))\n' ' val = pick(d, "valeur", "stat", "chiffre", "value")\n' ' desc = pick(d, "description", "texte", "label")\n' ' src = pick(d, "source", "reference")\n' ' # Hauteur réelle du bloc (C3b) : chiffre + desc ' '(+ source)\n' ' H_VAL, GAP_D, H_DESC, GAP_S, H_SRC = 5.1, 0.23, 2.0, ' '0.14, 0.9\n' ' block = H_VAL + GAP_D + H_DESC + (\n' ' GAP_S + H_SRC if src else 0.0)\n' ' y = self._cy(block)\n' ' self._text(slide, 0, y, self.SLIDE_W, H_VAL, val,\n' ' font=self.F_DISPLAY, size=self.T["stat_hero"], ' 'bold=True,\n' ' color=self.C["coral"], align=PP_ALIGN.CENTER,\n' ' anchor=MSO_ANCHOR.MIDDLE)\n' ' y_desc = y + H_VAL + GAP_D\n' ' self._text(slide, 6.86, y_desc, self.SLIDE_W - 13.72, ' 'H_DESC, desc,\n' ' size=18, color=self.C["body"], ' 'align=PP_ALIGN.CENTER)\n' ' if src:\n' ' y_src = y_desc + H_DESC + GAP_S\n' ' self._text(slide, 6.86, y_src, self.SLIDE_W - 13.72, ' 'H_SRC, src,\n' ' size=11, italic=True, ' 'color=self.C["muted"],\n' ' align=PP_ALIGN.CENTER)\n', ), # P2 — executive_summary : bloc label+texte centré dans la carte ( ' self._text(slide, self.MX + 3.81, y + 0.56, 8.1, 1.0, ' 'label,\n' ' size=14, bold=True, color=col, ' 'char_spacing=3)\n' ' self._text(slide, self.MX + 3.81, y + 1.57,\n' ' self.SLIDE_W - 2 * self.MX - 5.33, 1.8, ' 'txt,\n' ' size=self.T["body"], ' 'color=self.C["body"])\n', ' # Bloc label+texte centré verticalement dans la carte ' '(C3b)\n' ' H_LBL, GAP_LT, H_TXT = 0.85, 0.18, 1.8\n' ' blk = H_LBL + GAP_LT + H_TXT\n' ' y_lbl = y + max(0.0, (ch - blk) / 2)\n' ' self._text(slide, self.MX + 3.81, y_lbl, 8.1, H_LBL, ' 'label,\n' ' size=14, bold=True, color=col, ' 'char_spacing=3,\n' ' anchor=MSO_ANCHOR.MIDDLE)\n' ' self._text(slide, self.MX + 3.81, y_lbl + H_LBL + ' 'GAP_LT,\n' ' self.SLIDE_W - 2 * self.MX - 5.33, H_TXT, ' 'txt,\n' ' size=self.T["body"], color=self.C["body"],' '\n' ' anchor=MSO_ANCHOR.MIDDLE)\n', ), ] MARKER = "Hauteur réelle du bloc (C3b)" def fail(msg): print(" ! %s" % msg) sys.exit(1) def main(): if not TARGET.exists(): fail("%s introuvable — lancer depuis le dossier du pipeline." % TARGET) content = TARGET.read_text(encoding="utf-8") if MARKER in content: fail("Déjà patché (C3b présent) — rien à faire.") for i, (old, _) in enumerate(PATCHES, 1): n = content.count(old) if n == 0: fail("Ancre du patch %d introuvable — moteur inattendu " "(version différente ?)." % i) if n > 1: fail("Ancre du patch %d non unique (%d occurrences)." % (i, n)) shutil.copy2(TARGET, str(TARGET) + ".bak-c3b") print(" + Sauvegarde : %s.bak-c3b" % TARGET) for old, new in PATCHES: content = content.replace(old, new) TARGET.write_text(content, encoding="utf-8") print(" + 2 patchs de centrage appliqués (big_stat, " "executive_summary).") try: py_compile.compile(str(TARGET), doraise=True) print(" + Compilation OK.") except py_compile.PyCompileError as e: shutil.copy2(str(TARGET) + ".bak-c3b", TARGET) fail("Erreur de compilation — fichier restauré :\n%s" % e) print("\n Re-rends et compare les previews : big_stat (chiffre " "mieux centré),") print(" executive_summary (texte centré dans les cartes). Vérifie " "aussi from_to_pairs") print(" (le rognage des labels a pu disparaître avec le fix polices " "C3).") if __name__ == "__main__": main()