From 89c365b0cae0b69d19b58a0b3b73bb9f33d17e20 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Sat, 9 May 2026 21:52:03 +0200 Subject: [PATCH] svg correction --- SVG_CONTENT_ARCHITECTURE.md | 43 ++++- .../svg/01_lire_ecrire_fraction_simple.svg | 79 ++++----- .../svg/02_representer_fraction_partage.svg | 119 ++++++-------- .../03_fraction_sur_demi_droite_graduee.svg | 88 ++++------ .../svg/04_fraction_superieure_a_un.svg | 150 +++++++----------- .../svg/05_comparer_fractions_simples.svg | 117 ++++++-------- .../06_encadrer_fraction_entre_entiers.svg | 111 +++++-------- ...ditionner_soustraire_fractions_simples.svg | 129 +++++++-------- .../svg/08_fraction_d_une_quantite.svg | 82 ++++------ backend/tools/validate_svg_quality.py | 135 ++++++++++++++++ 10 files changed, 519 insertions(+), 534 deletions(-) diff --git a/SVG_CONTENT_ARCHITECTURE.md b/SVG_CONTENT_ARCHITECTURE.md index 180150b..b0af17a 100644 --- a/SVG_CONTENT_ARCHITECTURE.md +++ b/SVG_CONTENT_ARCHITECTURE.md @@ -67,6 +67,48 @@ Le validateur doit refuser ou signaler : Le premier filet de sécurité est disponible dans `backend/tools/validate_svg_quality.py`. +## Règles anti-chevauchement globales + +Les retours sur les 8 supports fractions montrent que valider une fraction isolée ne suffit pas. +Chaque expression mathématique doit réserver une boîte complète avant d'être placée dans la card. + +Règles obligatoires : + +- toute `fraction-g` doit porter un `data-box="x y width height"` couvrant le numérateur, la barre, le dénominateur et la marge pédagogique ; +- aucun texte, opérateur, figure, point, disque, bande ou autre fraction ne peut intersecter cette boîte ; +- dans une expression, le curseur horizontal avance de la largeur réservée de la fraction plus un espacement minimal de 28 px pour du texte courant et 34 px pour les signes visibles ; +- les signes `+`, `-`, `=`, `<`, `>` sont centrés sur l'axe de la barre de fraction, pas sur la ligne de base des chiffres ; +- les signes de comparaison sont placés au milieu de l'espace entre les deux boîtes de fractions ; +- une fraction posée près d'une figure utilise une zone mathématique dédiée au-dessus, en dessous ou à côté de la figure ; elle ne flotte jamais dans la zone de dessin ; +- les lignes d'équations sont espacées d'au moins la hauteur réservée de la plus grande fraction de la ligne plus 18 px ; +- une fraction inline dans une phrase garde un espace réservé avant et après, même si le texte semble court ; +- les titres de card, légendes et encadrés `À retenir` sont des zones séparées : une fraction ne doit pas empiéter dessus. + +En pratique, chaque card doit être pensée en zones : + +1. zone titre ; +2. zone mathématique ; +3. zone figure ; +4. zone légende ou explication. + +Une génération est rejetée si une boîte mathématique traverse une autre zone. C'est la règle qui évite les cas vus dans les captures : fractions collées aux mots, fractions sur les figures, signes décentrés, et lignes de fractions qui passent sous les schémas. + +## Règles de composition des expressions + +Les expressions doivent être générées par un layout horizontal, jamais par coordonnées libres. + +Le layout prend une liste de tokens : + +- `fraction(n, d, size)` ; +- `operator("+")`, `operator("=")`, `operator("<")` ; +- `text("est juste après 1")`. + +Chaque token expose sa boîte. L'expression calcule ensuite les positions. +Les opérateurs ne sont pas des textes décoratifs : ils ont une boîte propre et une position centrée entre deux tokens mathématiques. + +Pour les équations sur plusieurs lignes, on compose chaque ligne séparément, puis on empile les lignes avec une hauteur constante. +On ne réduit jamais l'interligne après coup pour "faire rentrer" une card ; si ça ne rentre pas, on change la disposition. + ## Architecture cible courte À court terme : @@ -95,4 +137,3 @@ L'architecture actuelle est acceptable pour afficher et relire des supports déj Elle n'est pas suffisante pour produire régulièrement des contenus mathématiques propres. La bonne direction n'est pas de remplacer SVG, mais de déplacer l'intelligence avant le SVG final : composants mathématiques, contraintes géométriques, validation automatique, puis rendu. - diff --git a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/01_lire_ecrire_fraction_simple.svg b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/01_lire_ecrire_fraction_simple.svg index fd1c28b..5d3ddfa 100644 --- a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/01_lire_ecrire_fraction_simple.svg +++ b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/01_lire_ecrire_fraction_simple.svg @@ -1,55 +1,32 @@ - - - - - + + + Lire et écrire une fraction Maths CM1 - Numérateur, dénominateur, unité - - - - La fraction - 3 - - 4 - - - - Ce que ça dit - - 3 parts prises - - 4 parts égales - - - 3 - - 4 - - se lit : - - trois quarts - - - - L'unité partagée - - - - - - - - - - L'unité est coupée en 4 parts égales. - - - - À retenir - Le dénominateur indique le partage. Le numérateur indique les parts prises. - + + La fraction + 3 + + 4 + + Ce que ça dit + 3 parts prises + 4 parts égales + + 3 + + 4 + + se lit : trois quarts + + L'unité partagée + + + + + + +L'unité est coupée en 4 parts égales. + À retenirLe dénominateur indique le partage. Le numérateur indique les parts prises. diff --git a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/02_representer_fraction_partage.svg b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/02_representer_fraction_partage.svg index 9cca738..bfdd654 100644 --- a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/02_representer_fraction_partage.svg +++ b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/02_representer_fraction_partage.svg @@ -1,77 +1,48 @@ - - - - - + + + Représenter une fraction - Je partage l'unité en parts égales, puis je colorie - - - - - Bande : - - 3 - - 5 - - - - - - - - - - - - - 5 parts égales, 3 parts coloriées - - - - - Disque : - - 1 - - 4 - - - - - - - 4 parts égales, 1 part coloriée - - - - - Collection : - - 6 - - 10 - - - - - - - - - - - - - - - 10 objets, 6 sont choisis - - - - À retenir - Une fraction n'a de sens que si l'unité et les parts égales sont claires. - + Je partage l'unité en parts égales, puis je colorie + + +Bande : + + 3 + + 5 + + + + + + + + + +5 parts égales, 3 parts coloriées + +Disque : + + 1 + + 4 + + + + + 4 parts égales, 1 part coloriée + +Collection : + + 6 + + 10 + + + + + + 10 objets, 6 sont choisis + À retenirUne fraction n'a de sens que si l'unité et les parts égales sont claires. diff --git a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/03_fraction_sur_demi_droite_graduee.svg b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/03_fraction_sur_demi_droite_graduee.svg index 3c61147..c242c08 100644 --- a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/03_fraction_sur_demi_droite_graduee.svg +++ b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/03_fraction_sur_demi_droite_graduee.svg @@ -1,60 +1,38 @@ - - - - - + + + Placer une fraction sur une demi-droite Je partage chaque unité selon le dénominateur - - - - - Exemple : placer - - 5 - - 4 - - - - + + +Exemple : placer + + 5 + + 4 + + + - - 0 - - 1 - - 2 - - - - - - - - - - 5 - - 4 - - - - Dénominateur 4 : - chaque unité a 4 parts. - - - 5 - - 4 - - est juste après 1. - - - - - À retenir - L'intervalle de 0 à 1 est l'unité. Je le partage en parts égales. - + 0 + 1 + 2 + + + + + 5 + + 4 + +Dénominateur 4 :chaque unité a 4 parts. + + 5 + + 4 + + est juste après 1. + + À retenirL'intervalle de 0 à 1 est l'unité. Je le partage en parts égales. diff --git a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/04_fraction_superieure_a_un.svg b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/04_fraction_superieure_a_un.svg index 166cd53..96fa1fe 100644 --- a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/04_fraction_superieure_a_un.svg +++ b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/04_fraction_superieure_a_un.svg @@ -1,95 +1,63 @@ - - - - - + + + Fraction supérieure à 1 Quand le numérateur dépasse le dénominateur - - - - Exemple : - - 7 - - 4 - - - - - - - - - - - - - - - - - - - - - - - - Je décompose - - - 7 - - 4 - - = - - 4 - - 4 - - + - - 3 - - 4 - - - - - 7 - - 4 - - = 1 + - - 3 - - 4 - - - La première unité est complète. - Il reste encore trois quarts. - - - - À retenir - Une fraction peut être plus grande que 1 : il faut alors utiliser plusieurs unités. - + + +Exemple : + + 7 + + 4 + + + + + + + + + + + + + + + + + + Je décompose + + 7 + + 4 + += + + 4 + + 4 + ++ + + 3 + + 4 + + + + 7 + + 4 + += 1 + + + 3 + + 4 + +La première unité est complète.Il reste encore trois quarts. + À retenirUne fraction peut être plus grande que 1 : il faut alors utiliser plusieurs unités. diff --git a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/05_comparer_fractions_simples.svg b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/05_comparer_fractions_simples.svg index 24387ee..393c20a 100644 --- a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/05_comparer_fractions_simples.svg +++ b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/05_comparer_fractions_simples.svg @@ -1,73 +1,52 @@ - - - - - + + + Comparer des fractions simples Même unité, parts égales, puis je compare - - - - Même dénominateur - - - 3 - - 5 - - < - - 4 - - 5 - - - - - - - - - - - - - - - - - - - - - - - - - - - Même numérateur - - - 1 - - 2 - - > - - 1 - - 4 - - - Quand on partage en plus de parts, - chaque part est plus petite. - Toujours comparer la même unité. - - - - À retenir - À dénominateur égal, le plus grand numérateur donne la plus grande fraction. - + + Même dénominateur + + 3 + + 5 + +< + + 4 + + 5 + + + + + + + + + + + + + + + + + + + + Même numérateur + + 1 + + 2 + +> + + 1 + + 4 + +Quand on partage en plus de parts,chaque part est plus petite.Toujours comparer la même unité. + À retenirÀ dénominateur égal, le plus grand numérateur donne la plus grande fraction. diff --git a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/06_encadrer_fraction_entre_entiers.svg b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/06_encadrer_fraction_entre_entiers.svg index 17562c2..b9855da 100644 --- a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/06_encadrer_fraction_entre_entiers.svg +++ b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/06_encadrer_fraction_entre_entiers.svg @@ -1,72 +1,47 @@ - - - - - + + + Encadrer une fraction Je cherche entre quels entiers elle se trouve - - - - - Exemple : - - 7 - - 3 - - - - - 6 - - 3 - - = 2 - - - - 9 - - 3 - - = 3 - - - 2 < - - 7 - - 3 - - < 3 - - - - - - 0 - - 1 - - 2 - - 3 - - - - - 7 - - 3 - - - - - - - À retenir - Je compare la fraction aux fractions qui valent les entiers voisins. - + + +Exemple : + + 7 + + 3 + + + + 0123 + + 7 + + 3 + + + + 6 + + 3 + += 2 + + + 9 + + 3 + += 3 + +2 < + + 7 + + 3 + +< 3 + + À retenirJe compare la fraction aux fractions qui valent les entiers voisins. diff --git a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/07_additionner_soustraire_fractions_simples.svg b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/07_additionner_soustraire_fractions_simples.svg index 2c15940..527cf8b 100644 --- a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/07_additionner_soustraire_fractions_simples.svg +++ b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/07_additionner_soustraire_fractions_simples.svg @@ -1,78 +1,59 @@ - - - - - + + + Additionner des fractions simples Si les dénominateurs sont les mêmes, les parts ont la même taille - - - - Exemple - - - 2 - - 7 - - + - - 3 - - 7 - - = - - 5 - - 7 - - - - - - - - - - - - - - - - - - - - La règle - Je garde le dénominateur. - J'ajoute ou je retire - seulement les numérateurs. - - - 6 - - 8 - - - - - 2 - - 8 - - = - - 4 - - 8 - - - - - - À retenir - Avant de calculer, je vérifie que les dénominateurs sont identiques. - + + Exemple + + 2 + + 7 + ++ + + 3 + + 7 + += + + 5 + + 7 + + + + + + + + + + + + + + + La règleJe garde le dénominateur.J'ajoute ou je retireseulement les numérateurs. + + 6 + + 8 + +- + + 2 + + 8 + += + + 4 + + 8 + + + À retenirAvant de calculer, je vérifie que les dénominateurs sont identiques. diff --git a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/08_fraction_d_une_quantite.svg b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/08_fraction_d_une_quantite.svg index af3551c..3fd4130 100644 --- a/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/08_fraction_d_une_quantite.svg +++ b/backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions/svg/08_fraction_d_une_quantite.svg @@ -1,54 +1,34 @@ - - - - - - Trouver une fraction d'une quantité + + + + Trouver une fraction d'une quantité Je partage, puis je prends les parts demandées - - - - - Exemple : - - 3 - - 4 - - de 20 - - - - - - - - - - - 20 partage en 4 parts : chaque part vaut 5. - Je prends 3 parts : 15. - - - - La méthode - 20 ÷ 4 = 5 - 5 x 3 = 15 - Le dénominateur partage. - Le numérateur dit combien prendre. - - - - À retenir - - Pour trouver - - 3 - - 4 - - d'une quantité : je divise par 4, puis je multiplie par 3. - - + + +Exemple : + + 3 + + 4 + +de 20 + + + + + + + +20 partagé en 4 parts : chaque part vaut 5.Je prends 3 parts : 15. + La méthode20 à 4 = 55 x 3 = 15Le dénominateur partage.Le numérateur dit combien prendre. + À retenir +Pour trouver + + 3 + + 4 + +d'une quantité : je divise par 4, puis je multiplie par 3. + diff --git a/backend/tools/validate_svg_quality.py b/backend/tools/validate_svg_quality.py index c60c16a..e3432de 100644 --- a/backend/tools/validate_svg_quality.py +++ b/backend/tools/validate_svg_quality.py @@ -10,7 +10,9 @@ from pathlib import Path SLASH_FRACTION_RE = re.compile(r"(? "Box": + return Box(self.kind, self.label, self.x1 - amount, self.y1 - amount, self.x2 + amount, self.y2 + amount) + + def local_name(tag: str) -> str: return tag.rsplit("}", 1)[-1] @@ -105,11 +120,122 @@ def estimated_text_width(text: str, size: float) -> float: return max(1, len(text.strip())) * size * 0.62 +def parse_translate(value: str | None) -> tuple[float, float]: + if not value: + return 0.0, 0.0 + match = TRANSLATE_RE.search(value) + if not match: + return 0.0, 0.0 + return parse_number(match.group(1)), parse_number(match.group(2), 0.0) + + +def overlaps(a: Box, b: Box) -> bool: + return a.x1 < b.x2 and a.x2 > b.x1 and a.y1 < b.y2 and a.y2 > b.y1 + + +def parse_data_box(node: ET.Element, ox: float, oy: float) -> Box | None: + match = DATA_BOX_RE.match(node.attrib.get("data-box") or "") + if not match: + return None + x, y, width, height = (parse_number(value) for value in match.groups()) + return Box("fraction", node_text(node) or "fraction", ox + x, oy + y, ox + x + width, oy + y + height) + + +def text_box(node: ET.Element, styles: dict[str, dict[str, str]], ox: float, oy: float) -> Box | None: + content = node_text(node) + if not content: + return None + size = font_size(node, styles) + width = estimated_text_width(content, size) + x = ox + text_x(node) + y = oy + text_y(node) + anchor = node.attrib.get("text-anchor", "start") + if anchor == "middle": + x1 = x - width / 2 + x2 = x + width / 2 + elif anchor == "end": + x1 = x - width + x2 = x + else: + x1 = x + x2 = x + width + return Box("text", content, x1, y - size * 0.85, x2, y + size * 0.25) + + +def shape_box(node: ET.Element, ox: float, oy: float) -> Box | None: + tag = local_name(node.tag) + classes = class_names(node) + if tag == "rect": + if classes.intersection({"bg", "panel", "soft", "card", "white"}): + return None + x = ox + parse_number(node.attrib.get("x")) + y = oy + parse_number(node.attrib.get("y")) + return Box("shape", "rect", x, y, x + parse_number(node.attrib.get("width")), y + parse_number(node.attrib.get("height"))) + if tag == "circle": + cx = ox + parse_number(node.attrib.get("cx")) + cy = oy + parse_number(node.attrib.get("cy")) + radius = parse_number(node.attrib.get("r")) + return Box("shape", "circle", cx - radius, cy - radius, cx + radius, cy + radius) + return None + + +def collect_layout_boxes( + node: ET.Element, + styles: dict[str, dict[str, str]], + ox: float = 0.0, + oy: float = 0.0, + inside_fraction: bool = False, +) -> list[Box]: + tx, ty = parse_translate(node.attrib.get("transform")) + ox += tx + oy += ty + + if local_name(node.tag) == "g" and has_class(node, "fraction-g"): + box = parse_data_box(node, ox, oy) + return [box] if box else [] + + boxes: list[Box] = [] + if not inside_fraction: + if local_name(node.tag) == "text": + box = text_box(node, styles, ox, oy) + if box: + boxes.append(box) + else: + box = shape_box(node, ox, oy) + if box: + boxes.append(box) + + for child in node: + boxes.extend(collect_layout_boxes(child, styles, ox, oy, inside_fraction)) + return boxes + + +def validate_layout_collisions(path: Path, root: ET.Element, styles: dict[str, dict[str, str]]) -> list[Issue]: + issues: list[Issue] = [] + boxes = collect_layout_boxes(root, styles) + for index, first in enumerate(boxes): + for second in boxes[index + 1 :]: + if first.kind != "fraction" and second.kind != "fraction": + continue + if overlaps(first, second): + issues.append( + Issue( + "error", + path, + f"Chevauchement autour d'une fraction: {first.kind} {first.label!r} avec {second.kind} {second.label!r}.", + ) + ) + return issues + + def validate_fraction_group(path: Path, group: ET.Element, styles: dict[str, dict[str, str]]) -> list[Issue]: issues: list[Issue] = [] texts = [node for node in group if local_name(node.tag) == "text"] lines = [node for node in group if local_name(node.tag) == "line"] + if "data-box" not in group.attrib: + issues.append(Issue("warning", path, "Une fraction-g doit declarer data-box pour les controles anti-chevauchement.")) + if len(texts) < 2 or not lines: return [ Issue( @@ -187,6 +313,7 @@ def validate_svg(path: Path) -> list[Issue]: issues.append(Issue("warning", path, "SVG sans viewBox : recadrage et responsive moins fiables.")) styles = parse_styles(root) + issues.extend(validate_layout_collisions(path, root, styles)) for node in root.iter(): if local_name(node.tag) == "text": @@ -208,6 +335,14 @@ def validate_svg(path: Path) -> list[Issue]: f"Encodage texte suspect dans le SVG: {content!r}.", ) ) + if SUSPICIOUS_REPLACEMENT_RE.search(content): + issues.append( + Issue( + "error", + path, + f"Caractère de remplacement suspect dans le texte SVG: {content!r}.", + ) + ) if SLASH_FRACTION_RE.search(content): issues.append( Issue(