198 lines
7.8 KiB
Python
198 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
patch_render_engine_c5c.py — Chantier C5 · lot 3 (agenda, pyramid)
|
|
==================================================================
|
|
Patch strict de render_engine_v2.py. Réintroduit les deux layouts v1
|
|
disparus à la refonte, restylés PR Editorial. Aucun prérequis sur les
|
|
lots 1/2 (aucun helper chart utilisé).
|
|
|
|
agenda (L40) — sommaire typographique aéré : badge numéroté (navy ;
|
|
corail si actif: true), label (bold navy si actif), durée
|
|
alignée à droite en muted, filet séparateur card_alt.
|
|
Bornes : 2-8 sections (troncature tracée au-delà).
|
|
pyramid (L41) — pyramide à degrés : rectangles empilés centrés,
|
|
largeurs FIXES par étage (jamais recalculées) :
|
|
3 niveaux : 44 / 72 / 100 %
|
|
4 niveaux : 40 / 60 / 80 / 100 %
|
|
Couleurs fixes du sommet à la base : navy, navy_light,
|
|
slate, glacier (texte blanc, navy sur glacier). Descriptions
|
|
optionnelles alignées à droite de chaque étage (la pyramide
|
|
passe alors à 58 % de largeur) ; sans description, pyramide
|
|
centrée sur 80 %.
|
|
|
|
Usage (dossier du pipeline, single-line) :
|
|
python3 patch_render_engine_c5c.py
|
|
Vérifie chaque ancre, écrit .bak-c5c, compile, idempotent.
|
|
"""
|
|
|
|
import py_compile
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
TARGET = Path("render_engine_v2.py")
|
|
|
|
COMPONENTS = ''' # ---------------- structure & concept (C5 lot 3) ----------------
|
|
AGENDA_MAX = 8
|
|
PYRAMID_WIDTHS = {3: (0.44, 0.72, 1.0),
|
|
4: (0.40, 0.60, 0.80, 1.0)}
|
|
|
|
def _render_agenda(self, slide, d):
|
|
"""Sommaire : badges numérotés, section active en corail."""
|
|
self._title(slide, pick(d, "titre", "title"))
|
|
sections = (d.get("sections") or [])[:self.AGENDA_MAX]
|
|
if len(d.get("sections") or []) > self.AGENDA_MAX:
|
|
print(f" ~ agenda : sections tronquées à {self.AGENDA_MAX}")
|
|
if len(sections) < 2:
|
|
self._text(slide, self.MX, self.CONT_Y, 12, 1.0,
|
|
"[agenda : 2 sections minimum]", size=14,
|
|
color=self.C["muted"])
|
|
return
|
|
n = len(sections)
|
|
gap = 0.5
|
|
row_h = min(1.9, (self.CONT_H - gap * (n - 1)) / n)
|
|
tot = n * row_h + (n - 1) * gap
|
|
y = self._cy(tot)
|
|
bd = self.components["badge"]["sizes"]["m"]
|
|
x0 = self.MX + 1.5
|
|
w = self.SLIDE_W - 2 * self.MX - 3.0
|
|
for i, s in enumerate(sections):
|
|
if not isinstance(s, dict):
|
|
s = {"label": str(s)}
|
|
actif = bool(s.get("actif"))
|
|
num = s.get("numero", i + 1)
|
|
fill = self.C["coral"] if actif else self.C["navy"]
|
|
self._badge(slide, x0 + bd / 2, y + row_h / 2, bd, num,
|
|
fill=fill, font_size=20)
|
|
self._text(slide, x0 + bd + 1.0, y, w - bd - 6.0, row_h,
|
|
as_label(s, "label", "titre"),
|
|
size=20 if actif else 19, bold=actif,
|
|
color=self.C["navy"] if actif
|
|
else self.C["body"],
|
|
anchor=MSO_ANCHOR.MIDDLE)
|
|
duree = pick(s, "duree", "duration")
|
|
if duree:
|
|
self._text(slide, x0 + w - 4.5, y, 4.5, row_h, duree,
|
|
size=13, italic=True, color=self.C["muted"],
|
|
align=PP_ALIGN.RIGHT,
|
|
anchor=MSO_ANCHOR.MIDDLE, fit=False)
|
|
if i < n - 1:
|
|
self._rect(slide, x0 + bd + 1.0,
|
|
y + row_h + gap / 2 - 0.015,
|
|
w - bd - 1.0, 0.03, self.C["card"])
|
|
y += row_h + gap
|
|
|
|
def _render_pyramid(self, slide, d):
|
|
"""Pyramide à degrés : largeurs fixes, couleurs fixes."""
|
|
self._title(slide, pick(d, "titre", "title"))
|
|
niveaux = (d.get("niveaux") or d.get("levels") or [])[:4]
|
|
if len(niveaux) < 3:
|
|
self._text(slide, self.MX, self.CONT_Y, 12, 1.0,
|
|
"[pyramid : 3 ou 4 niveaux]", size=14,
|
|
color=self.C["muted"])
|
|
return
|
|
n = len(niveaux)
|
|
widths = self.PYRAMID_WIDTHS[n]
|
|
colors = [self.C["navy"], self.C["navy2"],
|
|
self.C["slate"], self.C["glacier"]]
|
|
if n == 3:
|
|
colors = [self.C["navy"], self.C["navy2"],
|
|
self.C["glacier"]]
|
|
has_desc = any(isinstance(nv, dict) and pick(nv, "description")
|
|
for nv in niveaux)
|
|
zone_w = self.SLIDE_W - 2 * self.MX
|
|
pyr_w = zone_w * (0.58 if has_desc else 0.80)
|
|
pyr_x = self.MX if has_desc else \
|
|
self.MX + (zone_w - pyr_w) / 2
|
|
cx = pyr_x + pyr_w / 2
|
|
gap = 0.15
|
|
avail = self.CONT_H - 0.4
|
|
stage_h = (avail - gap * (n - 1)) / n
|
|
y = self._cy(avail) + 0.2
|
|
for i, nv in enumerate(niveaux):
|
|
if not isinstance(nv, dict):
|
|
nv = {"label": str(nv)}
|
|
wt = widths[i] * pyr_w
|
|
color = colors[i]
|
|
txt_color = self.C["navy"] if color == self.C["glacier"] \
|
|
else self.C["white"]
|
|
self._rect(slide, cx - wt / 2, y, wt, stage_h, color)
|
|
self._text(slide, cx - wt / 2 + 0.3, y, wt - 0.6, stage_h,
|
|
as_label(nv, "label", "titre"),
|
|
size=16, bold=True, color=txt_color,
|
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
|
desc = pick(nv, "description", "detail")
|
|
if desc:
|
|
dx = pyr_x + pyr_w + 1.2
|
|
self._text(slide, dx, y,
|
|
self.SLIDE_W - self.MX - dx, stage_h, desc,
|
|
size=13, color=self.C["body"],
|
|
anchor=MSO_ANCHOR.MIDDLE)
|
|
y += stage_h + gap
|
|
|
|
'''
|
|
|
|
PATCHES = [
|
|
# REGISTRY
|
|
(
|
|
" \"matrix_2x2\": \"_render_matrix_2x2\",\n",
|
|
" \"matrix_2x2\": \"_render_matrix_2x2\",\n"
|
|
" \"agenda\": \"_render_agenda\",\n"
|
|
" \"pyramid\": \"_render_pyramid\",\n",
|
|
),
|
|
]
|
|
|
|
MARKER = "_render_agenda"
|
|
ANCHOR_ORCH = " # ---------------- orchestration ----------------"
|
|
|
|
|
|
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é (_render_agenda 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." % i)
|
|
if n > 1:
|
|
fail("Ancre du patch %d non unique (%d occurrences)." % (i, n))
|
|
if content.count(ANCHOR_ORCH) != 1:
|
|
fail("Ancre de la section orchestration introuvable ou non "
|
|
"unique.")
|
|
|
|
shutil.copy2(TARGET, str(TARGET) + ".bak-c5c")
|
|
print(" + Sauvegarde : %s.bak-c5c" % TARGET)
|
|
|
|
for old, new in PATCHES:
|
|
content = content.replace(old, new)
|
|
|
|
lines = content.split("\n")
|
|
idx = next(i for i, l in enumerate(lines) if ANCHOR_ORCH in l)
|
|
lines[idx:idx] = COMPONENTS.split("\n")
|
|
content = "\n".join(lines)
|
|
|
|
TARGET.write_text(content, encoding="utf-8")
|
|
print(" + Renderers agenda + pyramid + REGISTRY appliqués.")
|
|
try:
|
|
py_compile.compile(str(TARGET), doraise=True)
|
|
print(" + Compilation OK.")
|
|
except py_compile.PyCompileError as e:
|
|
shutil.copy2(str(TARGET) + ".bak-c5c", TARGET)
|
|
fail("Erreur de compilation — fichier restauré :\n%s" % e)
|
|
print("\n Suite : python3 patch_layouts_c5c.py puis propagation "
|
|
"(prompt_injection_v2 + build_gallery).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|