feat: freeform palier 4 - derives de tokens, shapes, alpha, rotation, full_bleed (C6)
This commit is contained in:
+83
-15
@@ -1143,11 +1143,19 @@ def list_assets(proj: "Project"):
|
|||||||
VALID_TOKENS = {"navy", "navy_light", "coral", "glacier", "slate",
|
VALID_TOKENS = {"navy", "navy_light", "coral", "glacier", "slate",
|
||||||
"card", "white", "body", "muted"}
|
"card", "white", "body", "muted"}
|
||||||
VALID_BLOCK_TYPES = {"title", "heading", "text", "stat", "circle",
|
VALID_BLOCK_TYPES = {"title", "heading", "text", "stat", "circle",
|
||||||
"badge", "card", "rect", "line", "image"}
|
"badge", "card", "rect", "line", "image", "shape"}
|
||||||
|
VALID_FREE_SHAPES = {"chevron", "arrow", "triangle", "pill", "donut",
|
||||||
|
"bracket_left", "bracket_right", "oval", "diamond",
|
||||||
|
"hexagon", "parallelogram", "moon"}
|
||||||
|
FREE_TOKEN_RE = re.compile(
|
||||||
|
r"^(navy|navy_light|coral|glacier|slate|card|card_alt|white|body|muted)"
|
||||||
|
r"(@\d{1,3})?$")
|
||||||
|
|
||||||
|
|
||||||
def validate_freeform(raw: str):
|
def validate_freeform(raw: str):
|
||||||
"""Valide la structure d'un YAML freeform. Retourne (ok, message, data)."""
|
"""Valide un YAML freeform — palier 4 (C6), deux niveaux.
|
||||||
|
Retourne (ok, message, data). Les avertissements n'empêchent pas
|
||||||
|
le rendu ; ils sont intégrés au message."""
|
||||||
try:
|
try:
|
||||||
data = yaml.safe_load(extract_yaml(raw))
|
data = yaml.safe_load(extract_yaml(raw))
|
||||||
except yaml.YAMLError as e:
|
except yaml.YAMLError as e:
|
||||||
@@ -1157,35 +1165,95 @@ def validate_freeform(raw: str):
|
|||||||
slides = data["slides"]
|
slides = data["slides"]
|
||||||
if not slides:
|
if not slides:
|
||||||
return False, "Aucune slide.", None
|
return False, "Aucune slide.", None
|
||||||
errors = []
|
|
||||||
|
def color_ok(c):
|
||||||
|
return bool(FREE_TOKEN_RE.match(str(c).strip().lower()))
|
||||||
|
|
||||||
|
errors, warns = [], []
|
||||||
for i, s in enumerate(slides, 1):
|
for i, s in enumerate(slides, 1):
|
||||||
if s.get("layout") != "freeform":
|
if s.get("layout") != "freeform":
|
||||||
errors.append(f"Slide {i} : layout doit être 'freeform'")
|
errors.append(f"Slide {i} : layout doit être 'freeform'")
|
||||||
continue
|
continue
|
||||||
if s.get("mode") not in ("light", "dark"):
|
if s.get("mode") not in ("light", "dark", None):
|
||||||
errors.append(f"Slide {i} : mode doit être 'light' ou 'dark'")
|
errors.append(f"Slide {i} : mode doit être 'light' ou "
|
||||||
|
f"'dark'")
|
||||||
|
bg = s.get("background")
|
||||||
|
if bg and not color_ok(bg):
|
||||||
|
errors.append(f"Slide {i} : background '{bg}' hors charte "
|
||||||
|
f"(token ou token@NN)")
|
||||||
blocks = s.get("blocks", [])
|
blocks = s.get("blocks", [])
|
||||||
if not blocks:
|
if not blocks:
|
||||||
errors.append(f"Slide {i} : aucun bloc")
|
errors.append(f"Slide {i} : aucun bloc")
|
||||||
if len(blocks) > 8:
|
if len(blocks) > 15:
|
||||||
errors.append(f"Slide {i} : {len(blocks)} blocs (max 8)")
|
errors.append(f"Slide {i} : {len(blocks)} blocs (max 15)")
|
||||||
|
elif len(blocks) > 10:
|
||||||
|
warns.append(f"Slide {i} : {len(blocks)} blocs — pense "
|
||||||
|
f"respiration (10 max conseillé)")
|
||||||
for j, b in enumerate(blocks, 1):
|
for j, b in enumerate(blocks, 1):
|
||||||
|
loc = f"Slide {i} bloc {j}"
|
||||||
bt = (b.get("type") or "text").lower()
|
bt = (b.get("type") or "text").lower()
|
||||||
if bt not in VALID_BLOCK_TYPES:
|
if bt not in VALID_BLOCK_TYPES:
|
||||||
errors.append(f"Slide {i} bloc {j} : type '{bt}' invalide")
|
errors.append(f"{loc} : type '{bt}' invalide")
|
||||||
|
if bt == "shape":
|
||||||
|
sk = str(b.get("shape") or "").lower()
|
||||||
|
if sk and sk not in VALID_FREE_SHAPES:
|
||||||
|
errors.append(
|
||||||
|
f"{loc} : shape '{sk}' inconnue "
|
||||||
|
f"({', '.join(sorted(VALID_FREE_SHAPES))})")
|
||||||
col = float(b.get("col", 0)); w = float(b.get("w", 4))
|
col = float(b.get("col", 0)); w = float(b.get("w", 4))
|
||||||
row = float(b.get("row", 0)); h = float(b.get("h", 1))
|
row = float(b.get("row", 0)); h = float(b.get("h", 1))
|
||||||
if col + w > 12.01:
|
if col + w > 12.01:
|
||||||
errors.append(f"Slide {i} bloc {j} : déborde (col+w={col+w:.1f}>12)")
|
errors.append(f"{loc} : déborde "
|
||||||
|
f"(col+w={col + w:.1f}>12)")
|
||||||
if row + h > 12.01:
|
if row + h > 12.01:
|
||||||
errors.append(f"Slide {i} bloc {j} : déborde (row+h={row+h:.1f}>12)")
|
errors.append(f"{loc} : déborde "
|
||||||
color = b.get("color")
|
f"(row+h={row + h:.1f}>12)")
|
||||||
if color and color not in VALID_TOKENS:
|
for key in ("color", "text_color"):
|
||||||
errors.append(f"Slide {i} bloc {j} : couleur '{color}' hors charte")
|
c = b.get(key)
|
||||||
|
if c and not color_ok(c):
|
||||||
|
errors.append(f"{loc} : {key} '{c}' hors charte "
|
||||||
|
f"(token ou token@NN, jamais de hex)")
|
||||||
|
border = b.get("border")
|
||||||
|
if border is not None:
|
||||||
|
if not isinstance(border, dict):
|
||||||
|
errors.append(f"{loc} : border doit être "
|
||||||
|
f"{{color, weight}}")
|
||||||
|
elif border.get("color") and not color_ok(border["color"]):
|
||||||
|
errors.append(f"{loc} : border.color "
|
||||||
|
f"'{border['color']}' hors charte")
|
||||||
|
alpha = b.get("alpha")
|
||||||
|
if alpha is not None:
|
||||||
|
try:
|
||||||
|
if not 0 <= float(alpha) <= 100:
|
||||||
|
warns.append(f"{loc} : alpha {alpha} hors "
|
||||||
|
f"0-100 — sera clampé")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(f"{loc} : alpha '{alpha}' non "
|
||||||
|
f"numérique")
|
||||||
|
rot = b.get("rotation")
|
||||||
|
if rot is not None:
|
||||||
|
try:
|
||||||
|
if float(rot) % 15 != 0:
|
||||||
|
warns.append(f"{loc} : rotation {rot}° — sera "
|
||||||
|
f"arrondie au pas de 15°")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(f"{loc} : rotation '{rot}' non "
|
||||||
|
f"numérique")
|
||||||
|
radius = b.get("radius")
|
||||||
|
if radius is not None:
|
||||||
|
try:
|
||||||
|
if not 0 <= float(radius) <= 0.5:
|
||||||
|
warns.append(f"{loc} : radius {radius} hors "
|
||||||
|
f"0-0.5 — sera clampé")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(f"{loc} : radius '{radius}' non "
|
||||||
|
f"numérique")
|
||||||
if errors:
|
if errors:
|
||||||
return False, "\n ".join(errors), data
|
return False, "\n ".join(errors), data
|
||||||
return True, f"{len(slides)} slides freeform valides.", data
|
msg = f"{len(slides)} slides freeform valides."
|
||||||
|
if warns:
|
||||||
|
msg += "\n ⚠ " + "\n ⚠ ".join(warns)
|
||||||
|
return True, msg, data
|
||||||
|
|
||||||
def run_free_designer(session: AgentSession, markdown: str, proj: Project):
|
def run_free_designer(session: AgentSession, markdown: str, proj: Project):
|
||||||
"""Flux libre : le Free Designer produit directement le YAML freeform."""
|
"""Flux libre : le Free Designer produit directement le YAML freeform."""
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
patch_facilitator_c6.py — Chantier C6 (validateur freeform palier 4)
|
||||||
|
====================================================================
|
||||||
|
Patch strict de facilitator_v9.py :
|
||||||
|
P1. VALID_BLOCK_TYPES accepte le type « shape ».
|
||||||
|
P2. validate_freeform v2 — deux niveaux :
|
||||||
|
ERREURS (bloquantes) : type de bloc inconnu, shape inconnue,
|
||||||
|
couleur hors charte (hex, token inconnu, dérivé mal formé),
|
||||||
|
bloc hors slide, plus de 15 blocs, mode invalide, background
|
||||||
|
hors charte.
|
||||||
|
AVERTISSEMENTS (non bloquants, affichés) : plus de 10 blocs
|
||||||
|
(densité), alpha hors 0-100 (sera clampé), rotation non
|
||||||
|
multiple de 15° (sera arrondie), radius hors 0-0.5.
|
||||||
|
Le vocabulaire validé : tokens + dérivés token@NN, les 12 shapes
|
||||||
|
curées, full_bleed, border, dash — aligné sur le moteur C6.
|
||||||
|
|
||||||
|
Usage (dossier du pipeline, single-line) :
|
||||||
|
python3 patch_facilitator_c6.py
|
||||||
|
Vérifie chaque ancre, écrit .bak-fc6, compile, idempotent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import py_compile
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TARGET = Path("facilitator_v9.py")
|
||||||
|
|
||||||
|
OLD_TYPES = ('VALID_BLOCK_TYPES = {"title", "heading", "text", "stat", '
|
||||||
|
'"circle",\n'
|
||||||
|
' "badge", "card", "rect", "line", '
|
||||||
|
'"image"}\n')
|
||||||
|
|
||||||
|
NEW_TYPES = ('VALID_BLOCK_TYPES = {"title", "heading", "text", "stat", '
|
||||||
|
'"circle",\n'
|
||||||
|
' "badge", "card", "rect", "line", '
|
||||||
|
'"image", "shape"}\n'
|
||||||
|
'VALID_FREE_SHAPES = {"chevron", "arrow", "triangle", '
|
||||||
|
'"pill", "donut",\n'
|
||||||
|
' "bracket_left", "bracket_right", '
|
||||||
|
'"oval", "diamond",\n'
|
||||||
|
' "hexagon", "parallelogram", "moon"}\n'
|
||||||
|
'FREE_TOKEN_RE = re.compile(\n'
|
||||||
|
' r"^(navy|navy_light|coral|glacier|slate|card|card_alt'
|
||||||
|
'|white|body|muted)"\n'
|
||||||
|
' r"(@\\d{1,3})?$")\n')
|
||||||
|
|
||||||
|
NEW_VALIDATE = '''def validate_freeform(raw: str):
|
||||||
|
"""Valide un YAML freeform — palier 4 (C6), deux niveaux.
|
||||||
|
Retourne (ok, message, data). Les avertissements n'empêchent pas
|
||||||
|
le rendu ; ils sont intégrés au message."""
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(extract_yaml(raw))
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
return False, f"Erreur de syntaxe YAML : {e}", None
|
||||||
|
if not isinstance(data, dict) or "slides" not in data:
|
||||||
|
return False, "Clé 'slides' manquante.", None
|
||||||
|
slides = data["slides"]
|
||||||
|
if not slides:
|
||||||
|
return False, "Aucune slide.", None
|
||||||
|
|
||||||
|
def color_ok(c):
|
||||||
|
return bool(FREE_TOKEN_RE.match(str(c).strip().lower()))
|
||||||
|
|
||||||
|
errors, warns = [], []
|
||||||
|
for i, s in enumerate(slides, 1):
|
||||||
|
if s.get("layout") != "freeform":
|
||||||
|
errors.append(f"Slide {i} : layout doit être 'freeform'")
|
||||||
|
continue
|
||||||
|
if s.get("mode") not in ("light", "dark", None):
|
||||||
|
errors.append(f"Slide {i} : mode doit être 'light' ou "
|
||||||
|
f"'dark'")
|
||||||
|
bg = s.get("background")
|
||||||
|
if bg and not color_ok(bg):
|
||||||
|
errors.append(f"Slide {i} : background '{bg}' hors charte "
|
||||||
|
f"(token ou token@NN)")
|
||||||
|
blocks = s.get("blocks", [])
|
||||||
|
if not blocks:
|
||||||
|
errors.append(f"Slide {i} : aucun bloc")
|
||||||
|
if len(blocks) > 15:
|
||||||
|
errors.append(f"Slide {i} : {len(blocks)} blocs (max 15)")
|
||||||
|
elif len(blocks) > 10:
|
||||||
|
warns.append(f"Slide {i} : {len(blocks)} blocs — pense "
|
||||||
|
f"respiration (10 max conseillé)")
|
||||||
|
for j, b in enumerate(blocks, 1):
|
||||||
|
loc = f"Slide {i} bloc {j}"
|
||||||
|
bt = (b.get("type") or "text").lower()
|
||||||
|
if bt not in VALID_BLOCK_TYPES:
|
||||||
|
errors.append(f"{loc} : type '{bt}' invalide")
|
||||||
|
if bt == "shape":
|
||||||
|
sk = str(b.get("shape") or "").lower()
|
||||||
|
if sk and sk not in VALID_FREE_SHAPES:
|
||||||
|
errors.append(
|
||||||
|
f"{loc} : shape '{sk}' inconnue "
|
||||||
|
f"({', '.join(sorted(VALID_FREE_SHAPES))})")
|
||||||
|
col = float(b.get("col", 0)); w = float(b.get("w", 4))
|
||||||
|
row = float(b.get("row", 0)); h = float(b.get("h", 1))
|
||||||
|
if col + w > 12.01:
|
||||||
|
errors.append(f"{loc} : déborde "
|
||||||
|
f"(col+w={col + w:.1f}>12)")
|
||||||
|
if row + h > 12.01:
|
||||||
|
errors.append(f"{loc} : déborde "
|
||||||
|
f"(row+h={row + h:.1f}>12)")
|
||||||
|
for key in ("color", "text_color"):
|
||||||
|
c = b.get(key)
|
||||||
|
if c and not color_ok(c):
|
||||||
|
errors.append(f"{loc} : {key} '{c}' hors charte "
|
||||||
|
f"(token ou token@NN, jamais de hex)")
|
||||||
|
border = b.get("border")
|
||||||
|
if border is not None:
|
||||||
|
if not isinstance(border, dict):
|
||||||
|
errors.append(f"{loc} : border doit être "
|
||||||
|
f"{{color, weight}}")
|
||||||
|
elif border.get("color") and \
|
||||||
|
not color_ok(border["color"]):
|
||||||
|
errors.append(f"{loc} : border.color "
|
||||||
|
f"'{border['color']}' hors charte")
|
||||||
|
alpha = b.get("alpha")
|
||||||
|
if alpha is not None:
|
||||||
|
try:
|
||||||
|
if not 0 <= float(alpha) <= 100:
|
||||||
|
warns.append(f"{loc} : alpha {alpha} hors "
|
||||||
|
f"0-100 — sera clampé")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(f"{loc} : alpha '{alpha}' non "
|
||||||
|
f"numérique")
|
||||||
|
rot = b.get("rotation")
|
||||||
|
if rot is not None:
|
||||||
|
try:
|
||||||
|
if float(rot) % 15 != 0:
|
||||||
|
warns.append(f"{loc} : rotation {rot}° — sera "
|
||||||
|
f"arrondie au pas de 15°")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(f"{loc} : rotation '{rot}' non "
|
||||||
|
f"numérique")
|
||||||
|
radius = b.get("radius")
|
||||||
|
if radius is not None:
|
||||||
|
try:
|
||||||
|
if not 0 <= float(radius) <= 0.5:
|
||||||
|
warns.append(f"{loc} : radius {radius} hors "
|
||||||
|
f"0-0.5 — sera clampé")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(f"{loc} : radius '{radius}' non "
|
||||||
|
f"numérique")
|
||||||
|
if errors:
|
||||||
|
return False, "\\n ".join(errors), data
|
||||||
|
msg = f"{len(slides)} slides freeform valides."
|
||||||
|
if warns:
|
||||||
|
msg += "\\n ⚠ " + "\\n ⚠ ".join(warns)
|
||||||
|
return True, msg, data
|
||||||
|
'''
|
||||||
|
|
||||||
|
MARKER = "VALID_FREE_SHAPES"
|
||||||
|
|
||||||
|
|
||||||
|
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é (VALID_FREE_SHAPES présent) — rien à faire.")
|
||||||
|
|
||||||
|
if content.count(OLD_TYPES) != 1:
|
||||||
|
fail("Ancre VALID_BLOCK_TYPES introuvable ou non unique — "
|
||||||
|
"appliquer d'abord le patch C4 du facilitator.")
|
||||||
|
|
||||||
|
# Localiser la fonction validate_freeform complète
|
||||||
|
start = content.find("def validate_freeform(raw: str):")
|
||||||
|
if start == -1:
|
||||||
|
fail("validate_freeform introuvable.")
|
||||||
|
end = content.find("\ndef ", start + 10)
|
||||||
|
if end == -1:
|
||||||
|
fail("Fin de validate_freeform introuvable.")
|
||||||
|
end += 1 # conserver le \\n final avant def suivant
|
||||||
|
|
||||||
|
shutil.copy2(TARGET, str(TARGET) + ".bak-fc6")
|
||||||
|
print(" + Sauvegarde : %s.bak-fc6" % TARGET)
|
||||||
|
|
||||||
|
content = content.replace(OLD_TYPES, NEW_TYPES)
|
||||||
|
# recalcul après P1
|
||||||
|
start = content.find("def validate_freeform(raw: str):")
|
||||||
|
end = content.find("\ndef ", start + 10) + 1
|
||||||
|
content = content[:start] + NEW_VALIDATE + "\n" + content[end:]
|
||||||
|
|
||||||
|
TARGET.write_text(content, encoding="utf-8")
|
||||||
|
print(" + Types + shapes + validateur 2 niveaux appliqués.")
|
||||||
|
try:
|
||||||
|
py_compile.compile(str(TARGET), doraise=True)
|
||||||
|
print(" + Compilation OK.")
|
||||||
|
except py_compile.PyCompileError as e:
|
||||||
|
shutil.copy2(str(TARGET) + ".bak-fc6", TARGET)
|
||||||
|
fail("Erreur de compilation — fichier restauré :\n%s" % e)
|
||||||
|
print("\n Prochaine étape : prompt_the_free_designer_v2.md dans "
|
||||||
|
"Mistral Studio (temp 0.7).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
patch_render_engine_c6.py — Chantier C6 (Freeform palier 4)
|
||||||
|
===========================================================
|
||||||
|
« Charte minimale, vocabulaire maximal. » Patch strict de
|
||||||
|
render_engine_v2.py. PRÉREQUIS : C5 lot 2 appliqué (_blend, MSO_LINE).
|
||||||
|
|
||||||
|
P1. _token_color v2 — dérivés paramétrés : 'navy@15' = navy éclairci
|
||||||
|
de 15 % vers blanc. L'espace couleur reste ENTIÈREMENT engendré
|
||||||
|
par la charte (aucun hex, jamais), mais passe de 9 couleurs à un
|
||||||
|
continuum de tons de marque. Regex stricte, inconnu → défaut.
|
||||||
|
|
||||||
|
P2. _render_freeform v2 :
|
||||||
|
- background: <token[@NN]> au niveau slide (remplace/complète
|
||||||
|
mode light|dark ; la couleur du texte par défaut s'adapte).
|
||||||
|
- full_bleed: true par bloc — la grille 12×12 mappe alors la
|
||||||
|
slide ENTIÈRE (aplats bord à bord, colonnes pleine hauteur).
|
||||||
|
- Nouveau type shape : chevron, arrow, triangle, pill, donut,
|
||||||
|
bracket_left/right, oval, diamond, hexagon, parallelogram,
|
||||||
|
moon (douzaine curée MSO_SHAPE) + texte centré optionnel.
|
||||||
|
- Propriétés transverses sur tout bloc : alpha (0-100),
|
||||||
|
rotation (arrondie au pas de 15°), border {color, weight},
|
||||||
|
radius (0-0.5, rects arrondis).
|
||||||
|
- line : diagonales natives (w ET h non nuls) + dash: true.
|
||||||
|
- Plafond moteur 15 blocs (troncature tracée — le validateur
|
||||||
|
du facilitator bloque avant en usage normal).
|
||||||
|
|
||||||
|
Rien n'est ouvert sur : les polices, les hex, le footer/logo.
|
||||||
|
|
||||||
|
Usage (dossier du pipeline, single-line) :
|
||||||
|
python3 patches/patch_render_engine_c6.py
|
||||||
|
(ou depuis la racine si le script y est copié)
|
||||||
|
Vérifie chaque ancre, écrit .bak-c6, compile, idempotent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import py_compile
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TARGET = Path("render_engine_v2.py")
|
||||||
|
|
||||||
|
OLD_TOKEN_COLOR = ''' def _token_color(self, token: str, default: str = None) -> str:
|
||||||
|
mapping = {
|
||||||
|
"navy": self.C["navy"], "navy_light": self.C["navy2"],
|
||||||
|
"coral": self.C["coral"], "glacier": self.C["glacier"],
|
||||||
|
"slate": self.C["slate"], "card": self.C["card"],
|
||||||
|
"white": self.C["white"], "body": self.C["body"],
|
||||||
|
"muted": self.C["muted"],
|
||||||
|
}
|
||||||
|
return mapping.get((token or "").strip().lower(),
|
||||||
|
default or self.C["navy"])
|
||||||
|
'''
|
||||||
|
|
||||||
|
NEW_TOKEN_COLOR = ''' _TOKEN_RE = re.compile(r"^([a-z_]+)(?:@(\\d{1,3}))?$")
|
||||||
|
|
||||||
|
def _token_color(self, token: str, default: str = None) -> str:
|
||||||
|
"""Tokens de la charte + dérivés paramétrés (C6, palier 4) :
|
||||||
|
'navy@15' = navy éclairci de 15 % vers blanc. L'espace couleur
|
||||||
|
reste engendré par la charte — jamais de hex."""
|
||||||
|
mapping = {
|
||||||
|
"navy": self.C["navy"], "navy_light": self.C["navy2"],
|
||||||
|
"coral": self.C["coral"], "glacier": self.C["glacier"],
|
||||||
|
"slate": self.C["slate"], "card": self.C["card"],
|
||||||
|
"card_alt": self.C["card_alt"],
|
||||||
|
"white": self.C["white"], "body": self.C["body"],
|
||||||
|
"muted": self.C["muted"],
|
||||||
|
}
|
||||||
|
m = self._TOKEN_RE.match((token or "").strip().lower())
|
||||||
|
if not m:
|
||||||
|
return default or self.C["navy"]
|
||||||
|
base = mapping.get(m.group(1))
|
||||||
|
if base is None:
|
||||||
|
return default or self.C["navy"]
|
||||||
|
if m.group(2) is not None:
|
||||||
|
pct = min(100, int(m.group(2)))
|
||||||
|
return _blend(base, "#FFFFFF", pct / 100.0)
|
||||||
|
return base
|
||||||
|
'''
|
||||||
|
|
||||||
|
OLD_FREEFORM_START = " def _render_freeform(self, slide, d):\n"
|
||||||
|
OLD_FREEFORM_END = (' '
|
||||||
|
'PP_ALIGN.LEFT)\n')
|
||||||
|
|
||||||
|
NEW_FREEFORM = ''' FREE_SHAPES = {
|
||||||
|
"chevron": MSO_SHAPE.CHEVRON,
|
||||||
|
"arrow": MSO_SHAPE.RIGHT_ARROW,
|
||||||
|
"triangle": MSO_SHAPE.ISOSCELES_TRIANGLE,
|
||||||
|
"pill": MSO_SHAPE.ROUNDED_RECTANGLE,
|
||||||
|
"donut": MSO_SHAPE.DONUT,
|
||||||
|
"bracket_left": MSO_SHAPE.LEFT_BRACKET,
|
||||||
|
"bracket_right": MSO_SHAPE.RIGHT_BRACKET,
|
||||||
|
"oval": MSO_SHAPE.OVAL,
|
||||||
|
"diamond": MSO_SHAPE.DIAMOND,
|
||||||
|
"hexagon": MSO_SHAPE.HEXAGON,
|
||||||
|
"parallelogram": MSO_SHAPE.PARALLELOGRAM,
|
||||||
|
"moon": MSO_SHAPE.MOON,
|
||||||
|
}
|
||||||
|
_DARK_BASES = {"navy", "navy_light", "slate", "body"}
|
||||||
|
FREE_MAX_BLOCKS = 15
|
||||||
|
|
||||||
|
def _set_fill_alpha(self, sh, alpha_pct):
|
||||||
|
"""Transparence du remplissage (voile) — silencieux si le
|
||||||
|
shape n'a pas de solidFill (textbox…)."""
|
||||||
|
try:
|
||||||
|
srgb = sh._element.spPr.find(qn("a:solidFill")).find(
|
||||||
|
qn("a:srgbClr"))
|
||||||
|
a = srgb.makeelement(
|
||||||
|
qn("a:alpha"),
|
||||||
|
{"val": str(int(max(0.0, min(100.0,
|
||||||
|
float(alpha_pct))) * 1000))})
|
||||||
|
srgb.append(a)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _free_geom(self, blk, full_bleed):
|
||||||
|
"""Grille 12×12 : zone utile par défaut, slide entière en
|
||||||
|
full_bleed."""
|
||||||
|
col = float(blk.get("col", 0))
|
||||||
|
row = float(blk.get("row", 0))
|
||||||
|
wc = float(blk.get("w", 4))
|
||||||
|
hr = float(blk.get("h", 1))
|
||||||
|
if full_bleed:
|
||||||
|
return ((col / self.FREE_COLS) * self.SLIDE_W,
|
||||||
|
(row / self.FREE_ROWS) * self.SLIDE_H,
|
||||||
|
(wc / self.FREE_COLS) * self.SLIDE_W,
|
||||||
|
(hr / self.FREE_ROWS) * self.SLIDE_H)
|
||||||
|
return (self._free_x(col), self._free_y(row),
|
||||||
|
self._free_w(wc), self._free_h(hr))
|
||||||
|
|
||||||
|
def _free_decorate(self, sh, blk):
|
||||||
|
"""Propriétés transverses du palier 4 : alpha, rotation (pas
|
||||||
|
de 15°), border {color, weight}, radius. Silencieux quand une
|
||||||
|
propriété ne s'applique pas au type de shape."""
|
||||||
|
if sh is None:
|
||||||
|
return
|
||||||
|
if blk.get("alpha") is not None:
|
||||||
|
self._set_fill_alpha(sh, blk["alpha"])
|
||||||
|
rot = blk.get("rotation")
|
||||||
|
if rot:
|
||||||
|
try:
|
||||||
|
sh.rotation = (round(float(rot) / 15.0) * 15) % 360
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
border = blk.get("border")
|
||||||
|
if isinstance(border, dict):
|
||||||
|
try:
|
||||||
|
sh.line.color.rgb = hex_to_rgb(self._token_color(
|
||||||
|
border.get("color"), self.C["navy"]))
|
||||||
|
sh.line.width = Pt(float(border.get("weight", 1.0)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if blk.get("radius") is not None:
|
||||||
|
try:
|
||||||
|
sh.adjustments[0] = max(0.0, min(
|
||||||
|
0.5, float(blk["radius"])))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _render_freeform(self, slide, d):
|
||||||
|
mode = d.get("mode", "light")
|
||||||
|
bg = d.get("background")
|
||||||
|
on_dark = (mode == "dark")
|
||||||
|
if bg:
|
||||||
|
self._bg(slide, self._token_color(bg, self.C["white"]))
|
||||||
|
m = self._TOKEN_RE.match(str(bg).strip().lower())
|
||||||
|
if m and m.group(1) in self._DARK_BASES:
|
||||||
|
pct = int(m.group(2)) if m.group(2) is not None else 0
|
||||||
|
on_dark = pct < 45
|
||||||
|
else:
|
||||||
|
on_dark = False
|
||||||
|
elif on_dark:
|
||||||
|
self._bg(slide, self.C["navy"])
|
||||||
|
default_text = self.C["white"] if on_dark else self.C["body"]
|
||||||
|
|
||||||
|
blocks = d.get("blocks", [])
|
||||||
|
if len(blocks) > self.FREE_MAX_BLOCKS:
|
||||||
|
print(f" ~ freeform : {len(blocks)} blocs → "
|
||||||
|
f"{self.FREE_MAX_BLOCKS} (plafond moteur)")
|
||||||
|
blocks = blocks[:self.FREE_MAX_BLOCKS]
|
||||||
|
|
||||||
|
for blk in blocks:
|
||||||
|
btype = (blk.get("type") or "text").lower()
|
||||||
|
fb = bool(blk.get("full_bleed"))
|
||||||
|
x, y, w, h = self._free_geom(blk, fb)
|
||||||
|
sh = None
|
||||||
|
|
||||||
|
if btype == "rect":
|
||||||
|
sh = self._rect(slide, x, y, w, h,
|
||||||
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["card"]),
|
||||||
|
rounded=blk.get("rounded", False))
|
||||||
|
|
||||||
|
elif btype == "card":
|
||||||
|
sh = self._card(slide, x, y, w, h,
|
||||||
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["card"]))
|
||||||
|
|
||||||
|
elif btype == "circle":
|
||||||
|
d_cm = min(w, h)
|
||||||
|
sh = self._oval(slide, x, y, d_cm,
|
||||||
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["coral"]))
|
||||||
|
|
||||||
|
elif btype == "shape":
|
||||||
|
kind = self.FREE_SHAPES.get(
|
||||||
|
str(blk.get("shape") or "oval").lower())
|
||||||
|
if kind is None:
|
||||||
|
print(f" ~ freeform : shape "
|
||||||
|
f"'{blk.get('shape')}' inconnue → oval")
|
||||||
|
kind = MSO_SHAPE.OVAL
|
||||||
|
sh = slide.shapes.add_shape(
|
||||||
|
kind, Cm(x), Cm(y), Cm(w), Cm(h))
|
||||||
|
sh.fill.solid()
|
||||||
|
sh.fill.fore_color.rgb = hex_to_rgb(
|
||||||
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["coral"]))
|
||||||
|
sh.line.fill.background()
|
||||||
|
sh.shadow.inherit = False
|
||||||
|
if blk.get("text"):
|
||||||
|
tf = sh.text_frame
|
||||||
|
tf.word_wrap = True
|
||||||
|
tf.text = str(blk["text"])
|
||||||
|
p = tf.paragraphs[0]
|
||||||
|
p.alignment = PP_ALIGN.CENTER
|
||||||
|
for r in p.runs:
|
||||||
|
r.font.name = self.F_BODY
|
||||||
|
r.font.size = Pt(int(blk.get("size", 14)))
|
||||||
|
r.font.bold = True
|
||||||
|
r.font.color.rgb = hex_to_rgb(
|
||||||
|
self._token_color(blk.get("text_color"),
|
||||||
|
self.C["white"]))
|
||||||
|
|
||||||
|
elif btype == "line":
|
||||||
|
sh = slide.shapes.add_connector(
|
||||||
|
1, Cm(x), Cm(y), Cm(x + w), Cm(y + h))
|
||||||
|
sh.line.color.rgb = hex_to_rgb(
|
||||||
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["muted"]))
|
||||||
|
sh.line.width = Pt(float(blk.get("weight", 1.25)))
|
||||||
|
if blk.get("dash"):
|
||||||
|
sh.line.dash_style = MSO_LINE.DASH
|
||||||
|
sh.shadow.inherit = False
|
||||||
|
sh = None # pas de décoration fill sur ligne
|
||||||
|
|
||||||
|
elif btype == "image":
|
||||||
|
sh = self._image(slide, x, y, w, h,
|
||||||
|
blk.get("image") or blk.get("src", ""),
|
||||||
|
fit=str(blk.get("fit") or "cover"))
|
||||||
|
|
||||||
|
elif btype == "badge":
|
||||||
|
d_cm = min(w, h)
|
||||||
|
sh = self._badge(slide, x + d_cm / 2, y + d_cm / 2,
|
||||||
|
d_cm, blk.get("text", ""),
|
||||||
|
fill=self._token_color(
|
||||||
|
blk.get("color"), self.C["navy"]))
|
||||||
|
|
||||||
|
elif btype == "stat":
|
||||||
|
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
|
||||||
|
font=self.F_DISPLAY,
|
||||||
|
size=int(blk.get("size", 72)),
|
||||||
|
bold=True,
|
||||||
|
color=self._token_color(
|
||||||
|
blk.get("color"), self.C["coral"]),
|
||||||
|
align=self._free_align(
|
||||||
|
blk.get("align", "left")),
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE)
|
||||||
|
|
||||||
|
elif btype in ("title", "heading"):
|
||||||
|
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
|
||||||
|
font=self.F_DISPLAY,
|
||||||
|
size=int(blk.get("size", 28)),
|
||||||
|
bold=True,
|
||||||
|
color=self._token_color(
|
||||||
|
blk.get("color"),
|
||||||
|
self.C["white"] if on_dark
|
||||||
|
else self.C["navy"]),
|
||||||
|
align=self._free_align(
|
||||||
|
blk.get("align", "left")),
|
||||||
|
anchor=MSO_ANCHOR.MIDDLE)
|
||||||
|
|
||||||
|
else: # text
|
||||||
|
font = self.F_DISPLAY if blk.get("serif") \
|
||||||
|
else self.F_BODY
|
||||||
|
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
|
||||||
|
font=font,
|
||||||
|
size=int(blk.get("size", 16)),
|
||||||
|
bold=blk.get("bold", False),
|
||||||
|
italic=blk.get("italic", False),
|
||||||
|
color=self._token_color(
|
||||||
|
blk.get("color"), default_text),
|
||||||
|
align=self._free_align(
|
||||||
|
blk.get("align", "left")),
|
||||||
|
anchor=MSO_ANCHOR.TOP)
|
||||||
|
|
||||||
|
self._free_decorate(sh, blk)
|
||||||
|
|
||||||
|
def _free_align(self, a: str):
|
||||||
|
return {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER,
|
||||||
|
"right": PP_ALIGN.RIGHT}.get((a or "left").lower(),
|
||||||
|
PP_ALIGN.LEFT)
|
||||||
|
'''
|
||||||
|
|
||||||
|
MARKER = "_free_decorate"
|
||||||
|
PREREQ = "_blend"
|
||||||
|
|
||||||
|
|
||||||
|
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é (_free_decorate présent) — rien à faire.")
|
||||||
|
if ("def " + PREREQ) not in content:
|
||||||
|
fail("Prérequis manquant : C5 lot 2 (_blend) doit être appliqué "
|
||||||
|
"avant C6.")
|
||||||
|
|
||||||
|
# P1 — _token_color
|
||||||
|
if content.count(OLD_TOKEN_COLOR) != 1:
|
||||||
|
fail("Ancre _token_color introuvable ou non unique — moteur "
|
||||||
|
"inattendu.")
|
||||||
|
# P2 — bloc freeform complet (de def _render_freeform à la fin de
|
||||||
|
# _free_align)
|
||||||
|
start = content.find(OLD_FREEFORM_START)
|
||||||
|
if start == -1:
|
||||||
|
fail("Ancre _render_freeform introuvable.")
|
||||||
|
end = content.find(OLD_FREEFORM_END, start)
|
||||||
|
if end == -1:
|
||||||
|
fail("Fin du bloc freeform (_free_align) introuvable — moteur "
|
||||||
|
"inattendu.")
|
||||||
|
end += len(OLD_FREEFORM_END)
|
||||||
|
|
||||||
|
shutil.copy2(TARGET, str(TARGET) + ".bak-c6")
|
||||||
|
print(" + Sauvegarde : %s.bak-c6" % TARGET)
|
||||||
|
|
||||||
|
content = content.replace(OLD_TOKEN_COLOR, NEW_TOKEN_COLOR)
|
||||||
|
# recalcul des offsets après P1
|
||||||
|
start = content.find(OLD_FREEFORM_START)
|
||||||
|
end = content.find(OLD_FREEFORM_END, start) + len(OLD_FREEFORM_END)
|
||||||
|
content = content[:start] + NEW_FREEFORM + content[end:]
|
||||||
|
|
||||||
|
TARGET.write_text(content, encoding="utf-8")
|
||||||
|
print(" + _token_color v2 (dérivés @NN) + freeform palier 4 "
|
||||||
|
"appliqués.")
|
||||||
|
try:
|
||||||
|
py_compile.compile(str(TARGET), doraise=True)
|
||||||
|
print(" + Compilation OK.")
|
||||||
|
except py_compile.PyCompileError as e:
|
||||||
|
shutil.copy2(str(TARGET) + ".bak-c6", TARGET)
|
||||||
|
fail("Erreur de compilation — fichier restauré :\n%s" % e)
|
||||||
|
print("\n Suite C6 : python3 patches/patch_facilitator_c6.py "
|
||||||
|
"(validateur 2 niveaux),")
|
||||||
|
print(" puis remplacer le prompt Free Designer dans Mistral Studio"
|
||||||
|
" (v2, temp 0.7).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
patch_render_engine_c6b.py — Finition C6 (ombres parasites des shapes)
|
||||||
|
======================================================================
|
||||||
|
Les blocs freeform « shape » (add_shape) portent un élément <p:style>
|
||||||
|
implicite que LibreOffice/PowerPoint interprètent avec une ombre par
|
||||||
|
défaut — shadow.inherit=False ne suffit pas. Ce patch retire l'élément
|
||||||
|
de style à la création : rendu strictement plat, conforme à la charte.
|
||||||
|
|
||||||
|
Usage (dossier du pipeline, single-line) :
|
||||||
|
python3 patch_render_engine_c6b.py
|
||||||
|
Prérequis : C6 appliqué. Vérifie l'ancre, écrit .bak-c6b, compile,
|
||||||
|
idempotent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import py_compile
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TARGET = Path("render_engine_v2.py")
|
||||||
|
|
||||||
|
PATCHES = [
|
||||||
|
(
|
||||||
|
' sh.line.fill.background()\n'
|
||||||
|
' sh.shadow.inherit = False\n'
|
||||||
|
' if blk.get("text"):\n',
|
||||||
|
' sh.line.fill.background()\n'
|
||||||
|
' sh.shadow.inherit = False\n'
|
||||||
|
' _st = sh._element.find(qn("p:style"))\n'
|
||||||
|
' if _st is not None: # style implicite = '
|
||||||
|
'ombre\n'
|
||||||
|
' sh._element.remove(_st)\n'
|
||||||
|
' if blk.get("text"):\n',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
' if blk.get("dash"):\n'
|
||||||
|
' sh.line.dash_style = MSO_LINE.DASH\n'
|
||||||
|
' sh.shadow.inherit = False\n'
|
||||||
|
' sh = None # pas de décoration fill sur '
|
||||||
|
'ligne\n',
|
||||||
|
' if blk.get("dash"):\n'
|
||||||
|
' sh.line.dash_style = MSO_LINE.DASH\n'
|
||||||
|
' sh.shadow.inherit = False\n'
|
||||||
|
' _st = sh._element.find(qn("p:style"))\n'
|
||||||
|
' if _st is not None:\n'
|
||||||
|
' sh._element.remove(_st)\n'
|
||||||
|
' sh = None # pas de décoration fill sur '
|
||||||
|
'ligne\n',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
MARKER = 'sh._element.remove(_st)'
|
||||||
|
|
||||||
|
|
||||||
|
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é — rien à faire.")
|
||||||
|
for i, (old, _) in enumerate(PATCHES, 1):
|
||||||
|
n = content.count(old)
|
||||||
|
if n == 0:
|
||||||
|
fail("Ancre %d introuvable — le patch C6 est-il appliqué ?"
|
||||||
|
% i)
|
||||||
|
if n > 1:
|
||||||
|
fail("Ancre %d non unique (%d occurrences)." % (i, n))
|
||||||
|
|
||||||
|
shutil.copy2(TARGET, str(TARGET) + ".bak-c6b")
|
||||||
|
for old, new in PATCHES:
|
||||||
|
content = content.replace(old, new)
|
||||||
|
TARGET.write_text(content, encoding="utf-8")
|
||||||
|
print(" + Style implicite retiré des shapes et lignes freeform.")
|
||||||
|
try:
|
||||||
|
py_compile.compile(str(TARGET), doraise=True)
|
||||||
|
print(" + Compilation OK.")
|
||||||
|
except py_compile.PyCompileError as e:
|
||||||
|
shutil.copy2(str(TARGET) + ".bak-c6b", TARGET)
|
||||||
|
fail("Erreur de compilation — fichier restauré :\n%s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
# THE FREE DESIGNER — v2 (palier 4)
|
||||||
|
# Sliding Pipeline v11 — Pernod Ricard · Flux libre
|
||||||
|
# Mistral Large · Temperature 0.7 · Format : YAML freeform
|
||||||
|
|
||||||
|
## RÔLE
|
||||||
|
|
||||||
|
Tu es The Free Designer. Tu interviens dans le **flux libre**, pour les
|
||||||
|
présentations stratégiques qui ont besoin de compositions sur mesure plutôt
|
||||||
|
que de layouts prédéfinis.
|
||||||
|
|
||||||
|
Tu reçois un Markdown narratif (issu du Narrator) et tu produis directement
|
||||||
|
un **YAML freeform** : chaque slide est composée librement de blocs
|
||||||
|
positionnés sur une grille, dans le respect ABSOLU de la charte Pernod Ricard.
|
||||||
|
|
||||||
|
Tu es un directeur artistique, pas un remplisseur de gabarits. Tu composes
|
||||||
|
avec audace — la charte est ton cadre, pas ta cage. Le validateur et la
|
||||||
|
prévisualisation te rattrapent : ose.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LA CHARTE PERNOD RICARD (IMPOSÉE — non négociable)
|
||||||
|
|
||||||
|
**Couleurs** — uniquement les tokens et leurs dérivés (JAMAIS de code hex) :
|
||||||
|
- `navy` : bleu nuit dominant (fonds sombres, titres)
|
||||||
|
- `navy_light` : navy plus clair (cercles décoratifs sur fond sombre)
|
||||||
|
- `coral` : accent unique (chiffres, badges, lignes d'accent)
|
||||||
|
- `glacier` : bleu clair (sous-titres sur fond sombre, 3e couleur)
|
||||||
|
- `slate` : gris ardoise (2e couleur de cycle)
|
||||||
|
- `card` / `card_alt` : fonds de cartes (gris chaud / bleuté)
|
||||||
|
- `white`, `body`, `muted` : blanc, texte courant, légendes
|
||||||
|
|
||||||
|
**NOUVEAU — les dérivés `token@NN`** : chaque token peut être éclairci vers
|
||||||
|
le blanc de NN % : `navy@15` (teinte légère), `navy@70` (pastel),
|
||||||
|
`coral@30`, `glacier@50`… Tu disposes d'un continuum de tons de marque,
|
||||||
|
comme un DA qui décline sa palette. C'est TA nuance ; le hex reste interdit.
|
||||||
|
|
||||||
|
**Règle d'accent** : le corail est rare et précieux. Un seul élément corail
|
||||||
|
dominant par slide. Le navy et ses dérivés dominent.
|
||||||
|
|
||||||
|
**Polices** (gérées automatiquement — tu ne les choisis jamais) :
|
||||||
|
serif (Cambria) pour `title`/`heading`/`stat` et `serif: true` ;
|
||||||
|
sans-serif (Calibri) pour le reste.
|
||||||
|
|
||||||
|
**Rythme sandwich** : ouvertures, transitions et conclusions sombres ;
|
||||||
|
contenu clair. Tu peux affiner avec `background:`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LA GRILLE
|
||||||
|
|
||||||
|
12 colonnes × 12 lignes sur la zone utile. Chaque bloc : `col`, `row`,
|
||||||
|
`w`, `h`. Jamais `col + w > 12` ni `row + h > 12`.
|
||||||
|
|
||||||
|
**NOUVEAU — `full_bleed: true`** sur un bloc : sa grille mappe alors la
|
||||||
|
slide ENTIÈRE, bord à bord (aplats pleine page, colonne de couleur pleine
|
||||||
|
hauteur, image jusqu'aux bords). Réserve-le aux fonds et grandes masses ;
|
||||||
|
le texte reste dans la zone utile.
|
||||||
|
|
||||||
|
**NOUVEAU — `background: <token[@NN]>`** au niveau de la slide : fond de
|
||||||
|
n'importe quel ton de marque (`background: navy@92` = presque blanc bleuté,
|
||||||
|
`background: card` = gris chaud). La couleur de texte par défaut s'adapte.
|
||||||
|
|
||||||
|
**Garde-fous de composition :**
|
||||||
|
- Laisse de l'air. Le vide est un outil. UN point focal par slide.
|
||||||
|
- Aligne les blocs entre eux (mêmes `col` ou mêmes `row`).
|
||||||
|
- Maximum 15 blocs ; au-delà de 10, tu reçois un avertissement — et tu
|
||||||
|
as probablement tort.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LES TYPES DE BLOCS
|
||||||
|
|
||||||
|
**Textes** (inchangés) :
|
||||||
|
- `title` — grand titre serif (défaut 28, `size:` libre)
|
||||||
|
- `heading` — intertitre serif
|
||||||
|
- `text` — corps sans-serif (`bold:`, `italic:`, `serif:`, `size:`, `align:`)
|
||||||
|
- `stat` — chiffre héros serif corail (défaut 72)
|
||||||
|
|
||||||
|
**Formes pleines** :
|
||||||
|
- `rect` — rectangle (`rounded: true`, `radius: 0-0.5`)
|
||||||
|
- `card` — carte avec ombre douce
|
||||||
|
- `circle` — cercle (diamètre = min(w, h))
|
||||||
|
- `badge` — pastille numérotée (`text:`)
|
||||||
|
- **NOUVEAU `shape`** — forme de la bibliothèque curée :
|
||||||
|
`chevron`, `arrow`, `triangle`, `pill`, `donut`, `bracket_left`,
|
||||||
|
`bracket_right`, `oval`, `diamond`, `hexagon`, `parallelogram`, `moon`.
|
||||||
|
Texte centré optionnel (`text:`, `text_color:`, `size:`).
|
||||||
|
|
||||||
|
**Lignes** :
|
||||||
|
- `line` — du coin (col,row) au coin (col+w, row+h). **NOUVEAU : les
|
||||||
|
diagonales sont permises** (w ET h non nuls) et `dash: true` pointille.
|
||||||
|
|
||||||
|
**Image** :
|
||||||
|
- `image` — fichier de assets/ (`image:`, `fit: cover|contain`).
|
||||||
|
|
||||||
|
**NOUVEAU — propriétés transverses** (sur tout bloc plein) :
|
||||||
|
- `alpha: 0-100` — transparence du remplissage (voiles, superpositions)
|
||||||
|
- `rotation:` — par pas de 15° (±15, 30, 45…) : dynamise un shape, un stat
|
||||||
|
- `border: {color: token, weight: pt}` — contour (rect/card/shape)
|
||||||
|
- `full_bleed: true` — voir grille
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LES ARCHÉTYPES — compose par variation, pas par règle
|
||||||
|
|
||||||
|
Cinq compositions de référence. Ne les copie pas : varie-les, hybride-les,
|
||||||
|
détourne-les selon le contenu.
|
||||||
|
|
||||||
|
### 1. Éditorial pleine page (ouverture de partie, idée forte)
|
||||||
|
```yaml
|
||||||
|
- layout: freeform
|
||||||
|
background: navy
|
||||||
|
blocks:
|
||||||
|
- {type: shape, shape: oval, col: 8, row: -0, w: 8, h: 8, full_bleed: true,
|
||||||
|
color: navy_light, alpha: 55}
|
||||||
|
- {type: line, col: 0, row: 3, w: 2, h: 0, color: coral, weight: 3}
|
||||||
|
- {type: title, text: "L'idée qui change tout", col: 0, row: 3.6, w: 8,
|
||||||
|
h: 3, size: 40}
|
||||||
|
- {type: text, text: "Une phrase d'appui, sobre, glacier.", col: 0,
|
||||||
|
row: 7, w: 7, h: 1.5, color: glacier, italic: true, size: 17}
|
||||||
|
```
|
||||||
|
→ masse décorative full_bleed voilée, filet corail, titre à gauche, air.
|
||||||
|
|
||||||
|
### 2. Poster chiffre héros (un chiffre qui doit marquer)
|
||||||
|
```yaml
|
||||||
|
- layout: freeform
|
||||||
|
background: navy@94
|
||||||
|
blocks:
|
||||||
|
- {type: shape, shape: donut, col: 7.5, row: 2, w: 4.5, h: 4.5,
|
||||||
|
color: coral@70, alpha: 60}
|
||||||
|
- {type: stat, text: "3,4 M€", col: 1, row: 3.5, w: 8, h: 4, size: 110,
|
||||||
|
color: coral}
|
||||||
|
- {type: text, text: "d'économies identifiées sur le cycle 2026",
|
||||||
|
col: 1, row: 8, w: 7, h: 1.2, size: 18, color: body}
|
||||||
|
- {type: text, text: "Source : contrôle de gestion", col: 1, row: 9.4,
|
||||||
|
w: 6, h: 1, size: 11, color: muted, italic: true}
|
||||||
|
```
|
||||||
|
→ fond teinté à peine perceptible, écho géométrique derrière le chiffre.
|
||||||
|
|
||||||
|
### 3. Diptyque asymétrique 4/8 (tension entre deux idées)
|
||||||
|
```yaml
|
||||||
|
- layout: freeform
|
||||||
|
blocks:
|
||||||
|
- {type: rect, col: 0, row: 0, w: 4, h: 12, full_bleed: true, color: navy}
|
||||||
|
- {type: heading, text: "Avant", col: 0.5, row: 4, w: 3, h: 1.5,
|
||||||
|
color: white, size: 24}
|
||||||
|
- {type: text, text: "Données dispersées, décisions lentes.", col: 0.5,
|
||||||
|
row: 5.6, w: 3, h: 3, color: glacier, size: 14}
|
||||||
|
- {type: heading, text: "Après", col: 5, row: 4, w: 6, h: 1.5, size: 24}
|
||||||
|
- {type: text, text: "Un actif gouverné qui accélère chaque arbitrage.",
|
||||||
|
col: 5, row: 5.6, w: 6, h: 3, size: 16}
|
||||||
|
- {type: shape, shape: chevron, col: 3.6, row: 5.2, w: 1.2, h: 1.2,
|
||||||
|
color: coral}
|
||||||
|
```
|
||||||
|
→ colonne navy pleine hauteur (full_bleed), chevron corail de passage.
|
||||||
|
|
||||||
|
### 4. Data-hero (une donnée mise en scène, pas un graphique)
|
||||||
|
```yaml
|
||||||
|
- layout: freeform
|
||||||
|
blocks:
|
||||||
|
- {type: shape, shape: triangle, col: 8, row: 1, w: 3.5, h: 3.5,
|
||||||
|
color: glacier@40, rotation: 15}
|
||||||
|
- {type: title, text: "La qualité progresse", col: 0, row: 1, w: 7, h: 2}
|
||||||
|
- {type: stat, text: "−61 %", col: 0, row: 3.6, w: 6, h: 3.2, size: 84}
|
||||||
|
- {type: text, text: "d'incidents qualité en huit trimestres", col: 0,
|
||||||
|
row: 7, w: 6, h: 1.2, size: 17}
|
||||||
|
- {type: line, col: 7, row: 9.5, w: 4, h: -3, color: navy, weight: 2.5}
|
||||||
|
- {type: circle, col: 10.7, row: 6.2, w: 0.55, h: 0.55, color: coral}
|
||||||
|
```
|
||||||
|
→ la diagonale ascendante DESSINE la tendance ; le point corail la termine.
|
||||||
|
|
||||||
|
### 5. Manifeste typographique (conviction, conclusion)
|
||||||
|
```yaml
|
||||||
|
- layout: freeform
|
||||||
|
background: navy
|
||||||
|
blocks:
|
||||||
|
- {type: shape, shape: bracket_left, col: 0.2, row: 2.5, w: 0.8, h: 7,
|
||||||
|
color: coral}
|
||||||
|
- {type: title, text: "La donnée n'est pas un coût.", col: 1.6, row: 3,
|
||||||
|
w: 10, h: 2.2, size: 36}
|
||||||
|
- {type: title, text: "C'est notre prochain avantage.", col: 1.6,
|
||||||
|
row: 5.4, w: 10, h: 2.2, size: 36, color: glacier}
|
||||||
|
- {type: text, text: "Data Governance — Comex, juillet 2026", col: 1.6,
|
||||||
|
row: 9.5, w: 8, h: 1, size: 12, color: muted}
|
||||||
|
```
|
||||||
|
→ deux lignes de force, un crochet corail, rien d'autre.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CE QU'IL NE FAUT JAMAIS FAIRE
|
||||||
|
|
||||||
|
- Un code hex, une couleur hors tokens/dérivés → REFUSÉ par le validateur.
|
||||||
|
- Choisir une police, toucher au footer ou au logo (gérés par le moteur).
|
||||||
|
- Référencer une image absente de la liste fournie en contexte.
|
||||||
|
- Remplir la grille : plus de 10 blocs = tu as raté ta hiérarchie.
|
||||||
|
- Plusieurs éléments corail dominants sur la même slide.
|
||||||
|
|
||||||
|
## FORMAT DE SORTIE
|
||||||
|
|
||||||
|
Uniquement le YAML, sans commentaire ni Markdown autour :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
titre_presentation: ...
|
||||||
|
slides:
|
||||||
|
- position: 1
|
||||||
|
layout: freeform
|
||||||
|
mode: dark # ou background: <token[@NN]>
|
||||||
|
blocks:
|
||||||
|
- {type: ..., col: ..., row: ..., w: ..., h: ..., ...}
|
||||||
|
```
|
||||||
+190
-40
@@ -1268,16 +1268,30 @@ class RenderEngineV2:
|
|||||||
FREE_ROWS = 12
|
FREE_ROWS = 12
|
||||||
|
|
||||||
# Tokens de couleur autorisés (charte imposée — aucune couleur arbitraire)
|
# Tokens de couleur autorisés (charte imposée — aucune couleur arbitraire)
|
||||||
|
_TOKEN_RE = re.compile(r"^([a-z_]+)(?:@(\d{1,3}))?$")
|
||||||
|
|
||||||
def _token_color(self, token: str, default: str = None) -> str:
|
def _token_color(self, token: str, default: str = None) -> str:
|
||||||
|
"""Tokens de la charte + dérivés paramétrés (C6, palier 4) :
|
||||||
|
'navy@15' = navy éclairci de 15 % vers blanc. L'espace couleur
|
||||||
|
reste engendré par la charte — jamais de hex."""
|
||||||
mapping = {
|
mapping = {
|
||||||
"navy": self.C["navy"], "navy_light": self.C["navy2"],
|
"navy": self.C["navy"], "navy_light": self.C["navy2"],
|
||||||
"coral": self.C["coral"], "glacier": self.C["glacier"],
|
"coral": self.C["coral"], "glacier": self.C["glacier"],
|
||||||
"slate": self.C["slate"], "card": self.C["card"],
|
"slate": self.C["slate"], "card": self.C["card"],
|
||||||
|
"card_alt": self.C["card_alt"],
|
||||||
"white": self.C["white"], "body": self.C["body"],
|
"white": self.C["white"], "body": self.C["body"],
|
||||||
"muted": self.C["muted"],
|
"muted": self.C["muted"],
|
||||||
}
|
}
|
||||||
return mapping.get((token or "").strip().lower(),
|
m = self._TOKEN_RE.match((token or "").strip().lower())
|
||||||
default or self.C["navy"])
|
if not m:
|
||||||
|
return default or self.C["navy"]
|
||||||
|
base = mapping.get(m.group(1))
|
||||||
|
if base is None:
|
||||||
|
return default or self.C["navy"]
|
||||||
|
if m.group(2) is not None:
|
||||||
|
pct = min(100, int(m.group(2)))
|
||||||
|
return _blend(base, "#FFFFFF", pct / 100.0)
|
||||||
|
return base
|
||||||
|
|
||||||
def _free_x(self, col: float) -> float:
|
def _free_x(self, col: float) -> float:
|
||||||
"""Colonne de grille (0-12) → position x en cm (dans la zone utile)."""
|
"""Colonne de grille (0-12) → position x en cm (dans la zone utile)."""
|
||||||
@@ -1298,88 +1312,224 @@ class RenderEngineV2:
|
|||||||
usable = self.FOOTER_Y - 0.4 - self.TITLE_Y
|
usable = self.FOOTER_Y - 0.4 - self.TITLE_Y
|
||||||
return (rows / self.FREE_ROWS) * usable
|
return (rows / self.FREE_ROWS) * usable
|
||||||
|
|
||||||
|
FREE_SHAPES = {
|
||||||
|
"chevron": MSO_SHAPE.CHEVRON,
|
||||||
|
"arrow": MSO_SHAPE.RIGHT_ARROW,
|
||||||
|
"triangle": MSO_SHAPE.ISOSCELES_TRIANGLE,
|
||||||
|
"pill": MSO_SHAPE.ROUNDED_RECTANGLE,
|
||||||
|
"donut": MSO_SHAPE.DONUT,
|
||||||
|
"bracket_left": MSO_SHAPE.LEFT_BRACKET,
|
||||||
|
"bracket_right": MSO_SHAPE.RIGHT_BRACKET,
|
||||||
|
"oval": MSO_SHAPE.OVAL,
|
||||||
|
"diamond": MSO_SHAPE.DIAMOND,
|
||||||
|
"hexagon": MSO_SHAPE.HEXAGON,
|
||||||
|
"parallelogram": MSO_SHAPE.PARALLELOGRAM,
|
||||||
|
"moon": MSO_SHAPE.MOON,
|
||||||
|
}
|
||||||
|
_DARK_BASES = {"navy", "navy_light", "slate", "body"}
|
||||||
|
FREE_MAX_BLOCKS = 15
|
||||||
|
|
||||||
|
def _set_fill_alpha(self, sh, alpha_pct):
|
||||||
|
"""Transparence du remplissage (voile) — silencieux si le
|
||||||
|
shape n'a pas de solidFill (textbox…)."""
|
||||||
|
try:
|
||||||
|
srgb = sh._element.spPr.find(qn("a:solidFill")).find(
|
||||||
|
qn("a:srgbClr"))
|
||||||
|
a = srgb.makeelement(
|
||||||
|
qn("a:alpha"),
|
||||||
|
{"val": str(int(max(0.0, min(100.0,
|
||||||
|
float(alpha_pct))) * 1000))})
|
||||||
|
srgb.append(a)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _free_geom(self, blk, full_bleed):
|
||||||
|
"""Grille 12×12 : zone utile par défaut, slide entière en
|
||||||
|
full_bleed."""
|
||||||
|
col = float(blk.get("col", 0))
|
||||||
|
row = float(blk.get("row", 0))
|
||||||
|
wc = float(blk.get("w", 4))
|
||||||
|
hr = float(blk.get("h", 1))
|
||||||
|
if full_bleed:
|
||||||
|
return ((col / self.FREE_COLS) * self.SLIDE_W,
|
||||||
|
(row / self.FREE_ROWS) * self.SLIDE_H,
|
||||||
|
(wc / self.FREE_COLS) * self.SLIDE_W,
|
||||||
|
(hr / self.FREE_ROWS) * self.SLIDE_H)
|
||||||
|
return (self._free_x(col), self._free_y(row),
|
||||||
|
self._free_w(wc), self._free_h(hr))
|
||||||
|
|
||||||
|
def _free_decorate(self, sh, blk):
|
||||||
|
"""Propriétés transverses du palier 4 : alpha, rotation (pas
|
||||||
|
de 15°), border {color, weight}, radius. Silencieux quand une
|
||||||
|
propriété ne s'applique pas au type de shape."""
|
||||||
|
if sh is None:
|
||||||
|
return
|
||||||
|
if blk.get("alpha") is not None:
|
||||||
|
self._set_fill_alpha(sh, blk["alpha"])
|
||||||
|
rot = blk.get("rotation")
|
||||||
|
if rot:
|
||||||
|
try:
|
||||||
|
sh.rotation = (round(float(rot) / 15.0) * 15) % 360
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
border = blk.get("border")
|
||||||
|
if isinstance(border, dict):
|
||||||
|
try:
|
||||||
|
sh.line.color.rgb = hex_to_rgb(self._token_color(
|
||||||
|
border.get("color"), self.C["navy"]))
|
||||||
|
sh.line.width = Pt(float(border.get("weight", 1.0)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if blk.get("radius") is not None:
|
||||||
|
try:
|
||||||
|
sh.adjustments[0] = max(0.0, min(
|
||||||
|
0.5, float(blk["radius"])))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _render_freeform(self, slide, d):
|
def _render_freeform(self, slide, d):
|
||||||
mode = d.get("mode", "light")
|
mode = d.get("mode", "light")
|
||||||
|
bg = d.get("background")
|
||||||
on_dark = (mode == "dark")
|
on_dark = (mode == "dark")
|
||||||
if on_dark:
|
if bg:
|
||||||
|
self._bg(slide, self._token_color(bg, self.C["white"]))
|
||||||
|
m = self._TOKEN_RE.match(str(bg).strip().lower())
|
||||||
|
if m and m.group(1) in self._DARK_BASES:
|
||||||
|
pct = int(m.group(2)) if m.group(2) is not None else 0
|
||||||
|
on_dark = pct < 45
|
||||||
|
else:
|
||||||
|
on_dark = False
|
||||||
|
elif on_dark:
|
||||||
self._bg(slide, self.C["navy"])
|
self._bg(slide, self.C["navy"])
|
||||||
default_text = self.C["white"] if on_dark else self.C["body"]
|
default_text = self.C["white"] if on_dark else self.C["body"]
|
||||||
|
|
||||||
for blk in d.get("blocks", []):
|
blocks = d.get("blocks", [])
|
||||||
|
if len(blocks) > self.FREE_MAX_BLOCKS:
|
||||||
|
print(f" ~ freeform : {len(blocks)} blocs → "
|
||||||
|
f"{self.FREE_MAX_BLOCKS} (plafond moteur)")
|
||||||
|
blocks = blocks[:self.FREE_MAX_BLOCKS]
|
||||||
|
|
||||||
|
for blk in blocks:
|
||||||
btype = (blk.get("type") or "text").lower()
|
btype = (blk.get("type") or "text").lower()
|
||||||
col = float(blk.get("col", 0))
|
fb = bool(blk.get("full_bleed"))
|
||||||
row = float(blk.get("row", 0))
|
x, y, w, h = self._free_geom(blk, fb)
|
||||||
w_cols = float(blk.get("w", 4))
|
sh = None
|
||||||
h_rows = float(blk.get("h", 1))
|
|
||||||
x, y = self._free_x(col), self._free_y(row)
|
|
||||||
w, h = self._free_w(w_cols), self._free_h(h_rows)
|
|
||||||
|
|
||||||
if btype == "rect":
|
if btype == "rect":
|
||||||
self._rect(slide, x, y, w, h,
|
sh = self._rect(slide, x, y, w, h,
|
||||||
self._token_color(blk.get("color"), self.C["card"]),
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["card"]),
|
||||||
rounded=blk.get("rounded", False))
|
rounded=blk.get("rounded", False))
|
||||||
|
|
||||||
elif btype == "card":
|
elif btype == "card":
|
||||||
self._card(slide, x, y, w, h,
|
sh = self._card(slide, x, y, w, h,
|
||||||
self._token_color(blk.get("color"), self.C["card"]))
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["card"]))
|
||||||
|
|
||||||
elif btype == "circle":
|
elif btype == "circle":
|
||||||
d_cm = min(w, h)
|
d_cm = min(w, h)
|
||||||
self._oval(slide, x, y, d_cm,
|
sh = self._oval(slide, x, y, d_cm,
|
||||||
self._token_color(blk.get("color"), self.C["coral"]))
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["coral"]))
|
||||||
|
|
||||||
|
elif btype == "shape":
|
||||||
|
kind = self.FREE_SHAPES.get(
|
||||||
|
str(blk.get("shape") or "oval").lower())
|
||||||
|
if kind is None:
|
||||||
|
print(f" ~ freeform : shape "
|
||||||
|
f"'{blk.get('shape')}' inconnue → oval")
|
||||||
|
kind = MSO_SHAPE.OVAL
|
||||||
|
sh = slide.shapes.add_shape(
|
||||||
|
kind, Cm(x), Cm(y), Cm(w), Cm(h))
|
||||||
|
sh.fill.solid()
|
||||||
|
sh.fill.fore_color.rgb = hex_to_rgb(
|
||||||
|
self._token_color(blk.get("color"),
|
||||||
|
self.C["coral"]))
|
||||||
|
sh.line.fill.background()
|
||||||
|
sh.shadow.inherit = False
|
||||||
|
_st = sh._element.find(qn("p:style"))
|
||||||
|
if _st is not None: # style implicite = ombre
|
||||||
|
sh._element.remove(_st)
|
||||||
|
if blk.get("text"):
|
||||||
|
tf = sh.text_frame
|
||||||
|
tf.word_wrap = True
|
||||||
|
tf.text = str(blk["text"])
|
||||||
|
p = tf.paragraphs[0]
|
||||||
|
p.alignment = PP_ALIGN.CENTER
|
||||||
|
for r in p.runs:
|
||||||
|
r.font.name = self.F_BODY
|
||||||
|
r.font.size = Pt(int(blk.get("size", 14)))
|
||||||
|
r.font.bold = True
|
||||||
|
r.font.color.rgb = hex_to_rgb(
|
||||||
|
self._token_color(blk.get("text_color"),
|
||||||
|
self.C["white"]))
|
||||||
|
|
||||||
elif btype == "line":
|
elif btype == "line":
|
||||||
ln = slide.shapes.add_connector(
|
sh = slide.shapes.add_connector(
|
||||||
1, Cm(x), Cm(y), Cm(x + w), Cm(y + h))
|
1, Cm(x), Cm(y), Cm(x + w), Cm(y + h))
|
||||||
ln.line.color.rgb = hex_to_rgb(
|
sh.line.color.rgb = hex_to_rgb(
|
||||||
self._token_color(blk.get("color"), self.C["muted"]))
|
self._token_color(blk.get("color"),
|
||||||
ln.line.width = Pt(float(blk.get("weight", 1.25)))
|
self.C["muted"]))
|
||||||
|
sh.line.width = Pt(float(blk.get("weight", 1.25)))
|
||||||
|
if blk.get("dash"):
|
||||||
|
sh.line.dash_style = MSO_LINE.DASH
|
||||||
|
sh.shadow.inherit = False
|
||||||
|
_st = sh._element.find(qn("p:style"))
|
||||||
|
if _st is not None:
|
||||||
|
sh._element.remove(_st)
|
||||||
|
sh = None # pas de décoration fill sur ligne
|
||||||
|
|
||||||
elif btype == "image":
|
elif btype == "image":
|
||||||
self._image(slide, x, y, w, h,
|
sh = self._image(slide, x, y, w, h,
|
||||||
blk.get("image") or blk.get("src", ""),
|
blk.get("image") or blk.get("src", ""),
|
||||||
fit=str(blk.get("fit") or "cover"))
|
fit=str(blk.get("fit") or "cover"))
|
||||||
|
|
||||||
elif btype == "badge":
|
elif btype == "badge":
|
||||||
d_cm = min(w, h)
|
d_cm = min(w, h)
|
||||||
self._badge(slide, x + d_cm / 2, y + d_cm / 2, d_cm,
|
sh = self._badge(slide, x + d_cm / 2, y + d_cm / 2,
|
||||||
blk.get("text", ""),
|
d_cm, blk.get("text", ""),
|
||||||
fill=self._token_color(blk.get("color"),
|
fill=self._token_color(
|
||||||
self.C["navy"]))
|
blk.get("color"), self.C["navy"]))
|
||||||
|
|
||||||
elif btype == "stat":
|
elif btype == "stat":
|
||||||
# Grand chiffre — display, corail par défaut
|
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
|
||||||
self._text(slide, x, y, w, h, blk.get("text", ""),
|
|
||||||
font=self.F_DISPLAY,
|
font=self.F_DISPLAY,
|
||||||
size=int(blk.get("size", 72)), bold=True,
|
size=int(blk.get("size", 72)),
|
||||||
color=self._token_color(blk.get("color"),
|
bold=True,
|
||||||
self.C["coral"]),
|
color=self._token_color(
|
||||||
align=self._free_align(blk.get("align", "left")),
|
blk.get("color"), self.C["coral"]),
|
||||||
|
align=self._free_align(
|
||||||
|
blk.get("align", "left")),
|
||||||
anchor=MSO_ANCHOR.MIDDLE)
|
anchor=MSO_ANCHOR.MIDDLE)
|
||||||
|
|
||||||
elif btype in ("title", "heading"):
|
elif btype in ("title", "heading"):
|
||||||
self._text(slide, x, y, w, h, blk.get("text", ""),
|
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
|
||||||
font=self.F_DISPLAY,
|
font=self.F_DISPLAY,
|
||||||
size=int(blk.get("size", 28)), bold=True,
|
size=int(blk.get("size", 28)),
|
||||||
|
bold=True,
|
||||||
color=self._token_color(
|
color=self._token_color(
|
||||||
blk.get("color"),
|
blk.get("color"),
|
||||||
self.C["white"] if on_dark else self.C["navy"]),
|
self.C["white"] if on_dark
|
||||||
align=self._free_align(blk.get("align", "left")),
|
else self.C["navy"]),
|
||||||
|
align=self._free_align(
|
||||||
|
blk.get("align", "left")),
|
||||||
anchor=MSO_ANCHOR.MIDDLE)
|
anchor=MSO_ANCHOR.MIDDLE)
|
||||||
|
|
||||||
else: # text
|
else: # text
|
||||||
# Police body, ou display si explicitement demandé
|
|
||||||
font = self.F_DISPLAY if blk.get("serif") else self.F_BODY
|
font = self.F_DISPLAY if blk.get("serif") else self.F_BODY
|
||||||
self._text(slide, x, y, w, h, blk.get("text", ""),
|
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
|
||||||
font=font,
|
font=font,
|
||||||
size=int(blk.get("size", 16)),
|
size=int(blk.get("size", 16)),
|
||||||
bold=blk.get("bold", False),
|
bold=blk.get("bold", False),
|
||||||
italic=blk.get("italic", False),
|
italic=blk.get("italic", False),
|
||||||
color=self._token_color(blk.get("color"),
|
color=self._token_color(
|
||||||
default_text),
|
blk.get("color"), default_text),
|
||||||
align=self._free_align(blk.get("align", "left")),
|
align=self._free_align(
|
||||||
|
blk.get("align", "left")),
|
||||||
anchor=MSO_ANCHOR.TOP)
|
anchor=MSO_ANCHOR.TOP)
|
||||||
|
|
||||||
|
self._free_decorate(sh, blk)
|
||||||
|
|
||||||
def _free_align(self, a: str):
|
def _free_align(self, a: str):
|
||||||
return {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER,
|
return {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER,
|
||||||
"right": PP_ALIGN.RIGHT}.get((a or "left").lower(),
|
"right": PP_ALIGN.RIGHT}.get((a or "left").lower(),
|
||||||
|
|||||||
Reference in New Issue
Block a user