Retour utilisateur : "passons a un veritable graphe visuel". Chaque nœud gagne une position x/y (document_engine/labels/scenario_config.py : un x/y manquant/invalide retombe sur un quadrillage en cascade derive de l'index du nœud, jamais (0, 0) pour tous les nœuds qui les empilerait au meme endroit). Cote editeur (static/document/js/document-editor.js) : les nœuds sont des cartes deplacables a la souris sur un canevas (glisser-deposer reel, meme principe que le glisser des formes libres), les choix relies a une cible sont dessines comme des fleches SVG etiquetees par leur texte (jamais un menu deroulant). Editer le texte/les choix d'un nœud se fait dans un panneau inspecteur (colonne de droite) pour le nœud selectionne ; relier un choix se fait en cliquant "Relier" puis le nœud cible sur le graphe (mode connexion, Echap annule sans fermer la modale). La modale generique (.docModal*) est agrandie specifiquement pour ce graphe (jusqu'a 1180px) sans toucher sa taille par defaut. Verifie via simulation DOM reelle (jsdom) : rendu des nœuds/positions, glisser-deposer avec persistance au relachement, traces des fleches SVG + etiquettes, workflow complet du mode connexion, suppression d'un nœud avec reparation des references pendantes, et les 3 façons de fermer la modale (bouton/fond/Echap) y compris l'annulation du mode connexion sans fermer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9.4 KiB
document_engine/rendering/
Rendu HTML du document — point d'entrée unique utilisé à la fois par le
canevas d'édition et par le Mode Aperçu (même fonction, voir
docs/plan/PLAN.md — "Aperçu réel").
render_document(elements: list[dict[str, Any]]) -> str
Assemble le document entier à partir de la liste à plat renvoyée par
document_engine.list_document_elements : regroupe les éléments par
parent_id, puis rend récursivement les éléments top-niveau dans
l'ordre (une rangée rend elle-même ses propres enfants côte à côte).
- Retour : le HTML complet du document.
- Exceptions : aucune.
render_document_element(el: dict[str, Any], children_by_parent: dict[int | None, list[dict[str, Any]]]) -> str
HTML d'un seul élément — dispatch par dict (table _RENDERERS) selon
el["kind"], plutôt qu'un enchaînement de if (mirroir de l'esprit de
game_engine/scenes/render_scene_object.py). children_by_parent est le
regroupement précalculé par render_document, transmis pour que les
rangées puissent rendre récursivement leurs enfants sans refaire le
regroupement à chaque appel.
- Retour : le HTML de cet élément (et de ses enfants s'il s'agit d'une rangée).
- Exceptions : aucune ; un
kindinconnu produit un blocdocUnknownvisible plutôt qu'une levée d'exception.
Détail des rendus par catégorie (fonctions privées, table de dispatch)
- Rangée (
row) : conteneur flex (gap/align-items/justify-contentréels depuisattributes), enfants rendus récursivement. - Formes (
rectangle/cercle/triangle/trait) :<div>positionné en absolu (x/y/width/height/rotation/z_indexréels) contenant un SVG (rect/circle/polygon/lineselon le type). - Texte (
titre/paragraphe) :<div>stylé selonstyle(préréglage taille/graisse/interligne) etbold/italic/underline/align/color. - Image :
<img>, ou un bloc placeholder sisrcest vide. - Bouton :
<button>avec sonlabelet undata-targetoptionnel. - Quiz : toujours une carte résumant la config réelle (nombre de
questions, total des points via
quiz_total_points, minuteur si activé) — sanitizée (sanitize_quiz_config) avant lecture, jamais un rendu direct d'attributesbrut. Si au moins une question existe, s'y ajoute (fonction privée_render_quiz_player) le questionnaire RÉEL et interactif affiché en Mode Aperçu (voir docs/plan/maquettes/ document-formation-web.html — référence visuelle), masqué en édition par CSS (.docQuizPlayer, voir static/document/document-editor.css) : les questions sont embarquées en JSON dans un attributdata-quiz-config(jamais un<script>par élément), échappé pour l'HTML (html.escape(..., quote=True)) — static/document/js/document-editor.js lit cet attribut et gère tout le déroulé (réponse/score/question suivante/résultat) côté client, sans aucun aller-retour serveur. - Association : toujours une carte résumant la config réelle (nombre
de paires) — sanitizée (
sanitize_association_config) avant lecture. Si au moins une paire existe, s'y ajoute (fonction privée_render_association_player) le plateau de glisser-déposer RÉEL et interactif affiché en Mode Aperçu, masqué en édition par CSS (.docAssocPlayer) : les deux colonnes (termes/correspondances) sont mélangées INDÉPENDAMMENT (random.shuffle, mélange d'affichage — voirCODE_QUALITY.md) puis embarquées en JSON dans un attributdata-assoc-config, échappé pour l'HTML — même principe que le Quiz, aucun aller-retour serveur pendant qu'on joue. Contrairement au Quiz, le plateau reste affiché en permanence une fois la partie terminée : seul le bouton "Recommencer" (.docMinigameRestartBar, partagé avec Memory) apparaît, jamais d'écran de résultat séparé qui le remplacerait. - Memory : toujours une carte résumant la config réelle (nombre de
cartes définies, mode paire/simple) — sanitizée
(
sanitize_memory_config) avant lecture. Si au moins une carte existe, s'y ajoute (fonction privée_render_memory_player) le plateau de retournement RÉEL et interactif : en mode"paire", chaque carte définie est DUPLIQUÉE en deux instances partageant le mêmecard_index(l'appariement se fait dessus, classique Memory) ; en mode"single", une seule instance par carte (simple retournement, sans appariement). Les instances sont mélangées (random.shuffle, mélange d'affichage — voirCODE_QUALITY.md) puis embarquées en JSON dans un attributdata-memory-config, échappé pour l'HTML — même principe que le Quiz/l'Association, aucun aller-retour serveur pendant qu'on joue. Comme l'Association, le plateau reste affiché une fois toutes les paires trouvées (ou toutes les cartes révélées en mode"single") : seul le bouton "Recommencer" (.docMinigameRestartBar) apparaît. - Mots mêlés : toujours une carte résumant la config réelle (nombre
de mots) — sanitizée (
sanitize_mots_config) avant lecture. Si au moins un mot existe, s'y ajoute (fonction privée_render_mots_player) la grille RÉELLE et interactive. La grille ET la position exacte de chaque mot sont calculées ICI côté serveur (_build_mots_grid, jamais recalculées côté client) : chaque mot est placé horizontalement, verticalement, ou en diagonale (haut-gauche→bas-droite ou haut-droite→bas-gauche — jamais à l'envers), les lettres restantes tirées au hasard (random.choice/random.randint, tirage de jeu — voirCODE_QUALITY.md) ; si un mot ne trouve pas sa place, la grille entière est agrandie et le placement retenté depuis zéro, plutôt que d'abandonner silencieusement ce mot. Le tout (grille + coordonnées de chaque mot) est embarqué en JSON dans un attributdata-mots-config, échappé pour l'HTML — même principe que les autres mini-jeux, aucun aller-retour serveur pendant qu'on joue :static/document/js/ document-editor.jscompare les coordonnées EXACTES sélectionnées par l'apprenant à celles de chaque mot (jamais une simple comparaison de texte, qui se tromperait sur des lettres partagées entre deux mots qui se croisent). Comme l'Association/Memory, la grille reste affichée une fois tous les mots trouvés : seul le bouton "Recommencer" (.docMinigameRestartBar) apparaît. - Scénario : toujours une carte résumant la config réelle (nombre de
scénarios) — sanitizée (
sanitize_scenario_config) avant lecture. Si au moins un scénario existe, s'y ajoute (fonction privée_render_scenario_player) la mise en situation RÉELLE et interactive : un scénario est un ARBRE DE DÉCISION (voirscenario_config.pypour la forme exacte —nodes[0]= situation initiale, chaque choix pointe vers un autre nœud viatarget_id, un nœud sans choix est une fin de branche), pas une simple question à une seule conséquence. Aucune notion de bonne/mauvaise réponse (retour utilisateur du 20/09/2026 : "il n'y a pas de notion vrai/faux, l'utilisateur observe les conséquences") : l'apprenant choisit une option, le texte du nœud visé REMPLACE l'affichage du nœud précédent (les boutons de choix disparaissent avec lui), et ainsi de suite jusqu'à une fin de branche — volontairement PAS le comportement du Quiz, où la question resterait affichée à côté d'un encart de feedback séparé. Une fois une fin de branche atteinte, passage au scénario (arbre) suivant. Les scénarios gardent l'ORDRE d'écriture du créateur (jamais mélangés, contrairement à Association/Memory/Mots mêlés — ce sont des mises en situation séquentielles, pas des éléments à faire correspondre/retrouver). Réutilise les classes visuelles du Quiz (.docQuizOptions/.docQuizNextBar/.docQuizQuestionText) plutôt que de dupliquer ces règles. L'arbre complet (tous les nœuds/choix de tous les scénarios) est embarqué en JSON dans un attributdata-scenario-config, échappé pour l'HTML — même principe que les autres mini-jeux, aucun aller-retour serveur pendant qu'on joue : toute la navigation dans l'arbre se fait côté client. Comme l'Association/Memory/Mots mêlés (mais contrairement au Quiz), le dernier scénario reste affiché une fois une fin de branche atteinte : seul le bouton "Recommencer" (.docMinigameRestartBar) apparaît. L'arbre lui-même se construit dans une MODALE dédiée depuis le panneau Propriétés (voirforgeDocOpenScenarioTreeModal, static/document/js/document-editor.js) — trop de structure (nœuds + choix + destinations) pour la colonne étroite du panneau Propriétés, contrairement aux autres mini-jeux. La modale est un VRAI graphe visuel (retour utilisateur du 21/09/2026) : chaque nœud a une positionx/y(voirscenario_config.py), affiché comme une carte déplaçable à la souris sur un canevas ; chaque choix relié à untarget_idest dessiné comme une flèche SVG étiquetée par son texte, jamais un simple menu déroulant. Éditer le texte/les choix d'un nœud se fait dans l'inspecteur (panneau de droite) du nœud sélectionné ; relier un choix se fait en cliquant "Relier" puis le nœud cible sur le graphe (mode connexion, Échap annule). - Autres mini-jeux (
zones) : carte placeholder portant le libellé du type (voirdocument_engine/labels/element_kind_labels.py) — emplacement réservé, formulaire de contenu dédié hors périmètre de cette passe.