A badge must keep the name whole and readable whatever its length. The pure layout picks the largest name size that fits in one to three lines, then lays rounds on one or two columns down to the floor size, then drops the event name from the footer and then the affiliation; past that it refuses. A round line too wide shortens its title only, keeping at least one grapheme of it, never the table or seat. Metrics are injected, so the same code lays out under Noto Sans and under a fixed-pitch test font. Checked: red first; 3299 node tests green from the index alone. --- FR --- [ADD] pdf : badge mis en page, ordre de sacrifice, métriques injectées Un badge doit garder le nom entier et lisible, quelle que soit sa longueur. La mise en page, pure, choisit le plus grand corps du nom qui tient en une à trois lignes, pose les tours sur une ou deux colonnes jusqu'au corps plancher, puis retire du pied le nom de l'événement, puis l'appartenance ; au-delà, elle refuse. Une ligne de tour trop large n'abrège que son titre, en garde au moins une grappe, jamais la table ni le siège. Les métriques sont injectées : le même code se met en page sous Noto Sans et sous une police d'épreuve à chasse fixe. Vérifié : rouge d'abord ; 3299 node verts depuis l'index seul. Assisted-by: Claude Opus 5.5
435 lines
20 KiB
JavaScript
435 lines
20 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// La mise en page d'un badge (§ 11.4, § 11.5) : où tombe chaque ligne de
|
||
// texte dans le cadre utile, à quel corps, en quelle graisse. Module pur :
|
||
// toute mesure de texte passe par les métriques reçues, celles de la police
|
||
// embarquée lues par pdf-lib (metriquesDe) ou celles d'une épreuve ; aucune
|
||
// cote de glyphe n'y est posée. Les cotes sont en millimètres, l'origine au
|
||
// coin haut gauche du cadre utile, y sur la ligne de base ; les corps en
|
||
// points.
|
||
//
|
||
// La page est un flux. En haut, le nom en gras, puis l'appartenance, puis
|
||
// les lignes de tour ; en bas, le pied, sa dernière ligne posée sur le bord
|
||
// du cadre. Chaque ligne occupe la hauteur que la police lui donne, de
|
||
// l'ascendante à la descendante : deux lignes empilées ne se recouvrent
|
||
// jamais, et une ligne absente laisse sa place à la suivante.
|
||
//
|
||
// Le nom se choisit d'abord, sur la seule largeur : il passe avant tout le
|
||
// reste (§ 11.4, rang 1). Pour chaque forme du nom, de la plus grande au
|
||
// plancher, les tours essaient leurs dispositions, une colonne du corps des
|
||
// tours au plancher, puis deux colonnes ; la première qui tient l'emporte.
|
||
// Le nom au plancher, le badge se dépouille dans l'ordre de sacrifice
|
||
// (décision 8). Retirer un texte ne vient qu'après avoir réduit tout ce qui
|
||
// se réduit.
|
||
import { constante } from '../geometrie/constantes.js';
|
||
import { BADGE } from '../geometrie/papiers.js';
|
||
import { cadreUtile } from './imposition.js';
|
||
|
||
/** Le point typographique en millimètres. */
|
||
const MM_PAR_PT = 25.4 / 72;
|
||
|
||
/** La marque d'abrègement d'une appartenance ou d'un titre. */
|
||
const MARQUE = '\u{2026}';
|
||
|
||
/** Le séparateur que les libellés d'une ligne de tour posent devant la
|
||
* place, puis devant le titre : « Tour 2 — table 5, siège 3 — titre ». */
|
||
const SEPARATEUR = ' \u{2014} ';
|
||
|
||
/** La tolérance d'une comparaison de cotes, en millimètres : une somme de
|
||
* hauteurs égale à la hauteur utile tient, malgré l'arrondi flottant. */
|
||
const TOLERANCE_MM = 1e-9;
|
||
|
||
// Une grappe : un point de code suivi de ses marques combinantes. Une coupe
|
||
// entre deux grappes ne sépare jamais une lettre de son accent ; une marque
|
||
// en tête de texte forme sa propre grappe.
|
||
const GRAPPE = /\P{M}\p{M}*|\p{M}+/gu;
|
||
|
||
/**
|
||
* Les mesures de texte dont la mise en page a besoin, en millimètres.
|
||
*
|
||
* @typedef {Object} Metriques
|
||
* @property {(texte: string, corps: number, gras: boolean) => number} largeur
|
||
* @property {(corps: number) => {ascendante: number, descendante: number, capitale: number}} hauteurs
|
||
* proportionnelles au corps ; la descendante est positive
|
||
* @property {(texte: string) => string[]} absents les caractères sans
|
||
* glyphe, une fois chacun, dans l'ordre de première rencontre
|
||
*/
|
||
|
||
// La police fontkit qu'une PDFFont de pdf-lib porte, quand elle a été
|
||
// embarquée d'un fichier par fontkit ; TypeError sinon (police standard,
|
||
// valeur quelconque).
|
||
function fonteDe(police, graisse) {
|
||
const fonte = police?.embedder?.font;
|
||
if (typeof fonte?.hasGlyphForCodePoint !== 'function' || !Number.isFinite(fonte?.unitsPerEm)) {
|
||
throw new TypeError(`metriquesDe : police ${graisse} embarquée par fontkit attendue`);
|
||
}
|
||
return fonte;
|
||
}
|
||
|
||
/**
|
||
* Les métriques des deux graisses de la police embarquée. La largeur est
|
||
* celle que pdf-lib dessine (widthOfTextAtSize : les avances des glyphes
|
||
* après substitution des ligatures). Les hauteurs viennent de la table de
|
||
* la police, la plus grande des deux graisses pour chacune : une ligne
|
||
* réservée à leur hauteur contient l'une comme l'autre. Un caractère est
|
||
* absent quand l'une des deux graisses n'a pas son glyphe.
|
||
*
|
||
* @param {{regulier: import('pdf-lib').PDFFont, gras: import('pdf-lib').PDFFont}} polices
|
||
* @returns {Metriques}
|
||
* @throws {TypeError} une police qui n'a pas été embarquée par fontkit
|
||
*/
|
||
export function metriquesDe({ regulier, gras }) {
|
||
const fontes = [fonteDe(regulier, 'regulier'), fonteDe(gras, 'gras')];
|
||
const rapport = (lire) => Math.max(...fontes.map((f) => lire(f) / f.unitsPerEm));
|
||
const ascendante = rapport((f) => f.ascent);
|
||
const descendante = rapport((f) => -f.descent);
|
||
const capitale = rapport((f) => f.capHeight);
|
||
// La largeur d'un texte au corps 1000, par graisse : pdf-lib la rend
|
||
// proportionnelle au corps (somme des avances × corps / 1000), et la mise en
|
||
// page mesure le même texte à chaque palier. Une mise en forme par fontkit
|
||
// par texte suffit ; le produit par corps / 1000 refait l'opération de
|
||
// pdf-lib à l'identique.
|
||
const avances = [new Map(), new Map()];
|
||
const largeurMille = (texte, enGras) => {
|
||
const memoire = avances[enGras ? 1 : 0];
|
||
let mille = memoire.get(texte);
|
||
if (mille === undefined) {
|
||
mille = (enGras ? gras : regulier).widthOfTextAtSize(texte, 1000);
|
||
memoire.set(texte, mille);
|
||
}
|
||
return mille;
|
||
};
|
||
return {
|
||
largeur: (texte, corps, enGras) => largeurMille(texte, enGras) * (corps / 1000) * MM_PAR_PT,
|
||
hauteurs: (corps) => ({
|
||
ascendante: ascendante * corps * MM_PAR_PT,
|
||
descendante: descendante * corps * MM_PAR_PT,
|
||
capitale: capitale * corps * MM_PAR_PT,
|
||
}),
|
||
absents: (texte) => {
|
||
const vus = new Set();
|
||
const absents = [];
|
||
for (const caractere of texte) {
|
||
if (vus.has(caractere)) continue;
|
||
vus.add(caractere);
|
||
const point = caractere.codePointAt(0);
|
||
if (!fontes.every((f) => f.hasGlyphForCodePoint(point))) absents.push(caractere);
|
||
}
|
||
return absents;
|
||
},
|
||
};
|
||
}
|
||
|
||
// Les corps d'une réduction par paliers d'un point, du départ au plancher,
|
||
// le plancher compris même quand l'écart n'est pas un nombre entier de
|
||
// points. Un départ sous le plancher ne garde que le plancher.
|
||
function paliers(depart, plancher) {
|
||
const corps = [];
|
||
for (let c = depart; c > plancher + TOLERANCE_MM; c -= 1) corps.push(c);
|
||
corps.push(plancher);
|
||
return corps;
|
||
}
|
||
|
||
/**
|
||
* La mise en page d'un badge, ou le refus qui dit pourquoi il ne tient pas.
|
||
*
|
||
* Nom : le corps de départ donne au nom la hauteur de capitale maximale du
|
||
* registre, et descend d'un point par palier jusqu'au plancher, dont la
|
||
* capitale vaut le plancher du registre ; les deux se convertissent par la
|
||
* capitale que les métriques donnent. Le nom tient d'abord sur une ligne,
|
||
* puis sur deux, puis sur trois, chaque fois au plus grand corps possible,
|
||
* coupé aux espaces ; à défaut seulement, coupé entre deux grappes, de deux
|
||
* à trois lignes. Quand le reste du badge ne tient pas sous ce nom, il
|
||
* descend d'un palier à la fois jusqu'au plancher, au même nombre de lignes
|
||
* au plus. Au plancher, ou au-delà de trois lignes, où il se replie au
|
||
* plancher sur autant de lignes qu'il lui faut, le badge perd le nom de
|
||
* l'événement (le pied devient le pied réduit), puis, s'il ne tient
|
||
* toujours pas, l'appartenance. Le nom ne s'abrège jamais.
|
||
*
|
||
* Appartenance : une ligne, abrégée par « … » au plus long préfixe qui
|
||
* tient. Pied : chaque ligne se replie comme le nom, rien ne s'y abrège.
|
||
*
|
||
* Tours : une colonne, du corps des tours à leur plancher, puis deux
|
||
* colonnes, la première rangée de haut en bas d'abord, séparées d'un
|
||
* cadratin. Chaque ligne est entière, sauf au plancher : une ligne trop
|
||
* large pour sa colonne y abrège son titre, jamais sa place ; une ligne sans
|
||
* titre trop large ne se pose pas. Aucun corps ne dépasse celui du nom.
|
||
*
|
||
* Refus : PLANCHE_NOM_TROP_LONG quand le nom dépasse trois lignes et que
|
||
* rien ne tient, PLANCHE_TROP_DE_TOURS {tours} sinon, tours étant le nombre
|
||
* de lignes de tour. src/pdf ne connaît pas la personne : l'appelant
|
||
* complète les détails.
|
||
*
|
||
* @param {{nom: string, lignes: string[], appartenance: string|null,
|
||
* pied: string[], piedReduit: string[]}} textes
|
||
* @param {Metriques} metriques
|
||
* @returns {{blocs: Array<{texte: string, corps: number, gras: boolean, x: number, y: number}>,
|
||
* colonnesTours: 1|2}
|
||
* | {refus: {code: string, details: object}}}
|
||
*/
|
||
export function mettreEnPage(textes, metriques) {
|
||
const { largeur: L, hauteur: H } = cadreUtile({ x: 0, y: 0, largeur: BADGE.largeur, hauteur: BADGE.hauteur });
|
||
const memoire = new Map();
|
||
const largeur = (texte, corps, gras) => {
|
||
const cle = `${gras ? 'g' : 'r'}${corps}\u{0}${texte}`;
|
||
if (!memoire.has(cle)) memoire.set(cle, metriques.largeur(texte, corps, gras));
|
||
return memoire.get(cle);
|
||
};
|
||
const tient = (texte, corps, gras, largeurMax = L) => largeur(texte, corps, gras) <= largeurMax + TOLERANCE_MM;
|
||
const pas = (corps) => {
|
||
const { ascendante, descendante } = metriques.hauteurs(corps);
|
||
return { ascendante, descendante, hauteur: ascendante + descendante };
|
||
};
|
||
|
||
// Les lignes du texte repliées dans la largeur, au plus `limite` ; null
|
||
// quand il en faut davantage, ou qu'une grappe seule déborde. Un mot qui
|
||
// ne tient pas seul sur une ligne se coupe entre deux grappes, sauf sous
|
||
// `motsEntiers`, où le repli échoue.
|
||
function replier(texte, corps, gras, limite, motsEntiers = false) {
|
||
const lignes = [];
|
||
let courante = '';
|
||
const clore = () => {
|
||
lignes.push(courante);
|
||
courante = '';
|
||
return lignes.length <= limite;
|
||
};
|
||
for (const mot of texte.split(' ')) {
|
||
if (mot === '') continue;
|
||
const avecMot = courante === '' ? mot : `${courante} ${mot}`;
|
||
if (tient(avecMot, corps, gras)) {
|
||
courante = avecMot;
|
||
continue;
|
||
}
|
||
if (courante !== '' && !clore()) return null;
|
||
if (tient(mot, corps, gras)) {
|
||
courante = mot;
|
||
continue;
|
||
}
|
||
if (motsEntiers) return null;
|
||
for (const grappe of mot.match(GRAPPE)) {
|
||
const avecGrappe = courante + grappe;
|
||
if (courante === '' || tient(avecGrappe, corps, gras)) {
|
||
courante = avecGrappe;
|
||
} else if (!clore()) {
|
||
return null;
|
||
} else {
|
||
courante = grappe;
|
||
}
|
||
if (!tient(courante, corps, gras)) return null;
|
||
}
|
||
}
|
||
if (courante !== '' && !clore()) return null;
|
||
return lignes;
|
||
}
|
||
|
||
// Le texte entier s'il tient, sinon son plus long préfixe, espaces finales
|
||
// retirées, suivi de la marque ; null si la marque seule déborde. Les
|
||
// avances sont positives ou nulles : la largeur d'un préfixe croît avec
|
||
// son nombre de grappes, une dichotomie trouve le plus long qui tient seul,
|
||
// et aucun préfixe plus long ne tient avec la marque. De là, le premier qui
|
||
// tient avec elle, en descendant.
|
||
function abreger(texte, corps, gras, largeurMax) {
|
||
if (tient(texte, corps, gras, largeurMax)) return texte;
|
||
const grappes = texte.match(GRAPPE);
|
||
const prefixe = (nombre) => grappes.slice(0, nombre).join('');
|
||
// Le préfixe vide tient, le texte entier non.
|
||
let bas = 0;
|
||
let haut = grappes.length - 1;
|
||
while (bas < haut) {
|
||
const milieu = Math.ceil((bas + haut) / 2);
|
||
if (tient(prefixe(milieu), corps, gras, largeurMax)) bas = milieu;
|
||
else haut = milieu - 1;
|
||
}
|
||
for (let nombre = bas; nombre > 0; nombre -= 1) {
|
||
const candidat = `${prefixe(nombre).replace(/ +$/u, '')}${MARQUE}`;
|
||
if (tient(candidat, corps, gras, largeurMax)) return candidat;
|
||
}
|
||
return tient(MARQUE, corps, gras, largeurMax) ? MARQUE : null;
|
||
}
|
||
|
||
// Une ligne de tour dans la largeur : entière si elle tient ; sinon
|
||
// abrégée dans son titre, qui suit le second séparateur, sa place (tour,
|
||
// table, siège) gardée entière et au moins une grappe du titre ; null pour
|
||
// une ligne sans titre, ou dont la place et une grappe, suivies de la
|
||
// marque, débordent. Le plus long préfixe qu'abreger rend ne couvre pas
|
||
// la place : aucun autre ne la couvre.
|
||
function abregerTitre(ligne, corps, largeurMax) {
|
||
if (tient(ligne, corps, false, largeurMax)) return ligne;
|
||
const premier = ligne.indexOf(SEPARATEUR);
|
||
const finDeLaPlace = premier < 0 ? -1 : ligne.indexOf(SEPARATEUR, premier + SEPARATEUR.length);
|
||
if (finDeLaPlace < 0) return null;
|
||
const abregee = abreger(ligne, corps, false, largeurMax);
|
||
// Au moins une grappe du titre reste : la place suivie du seul séparateur
|
||
// et de la marque se lirait comme une place abrégée.
|
||
const titreGarde = abregee !== null && abregee.length - MARQUE.length > finDeLaPlace + SEPARATEUR.trimEnd().length;
|
||
return titreGarde ? abregee : null;
|
||
}
|
||
|
||
// Les formes du nom, dans l'ordre où le badge les essaie, chacune
|
||
// {corps, lignes}. La première est la plus grande qui tient en largeur :
|
||
// de une à trois lignes, du corps maximal au plancher, coupées aux
|
||
// espaces ; à défaut seulement, coupées entre deux grappes. Suivent les
|
||
// corps plus petits jusqu'au plancher, au même nombre de lignes au plus,
|
||
// qui libèrent de la hauteur. Au-delà de trois lignes, une forme seule :
|
||
// le plancher, sur autant de lignes qu'il en faut, bornées par ce que la
|
||
// hauteur utile empile.
|
||
const capitaleParPt = metriques.hauteurs(1).capitale;
|
||
const corpsNomMax = constante('HAUTEUR_CAPITALE_NOM_MAX_MM') / capitaleParPt;
|
||
const corpsNomPlancher = constante('HAUTEUR_CAPITALE_NOM_PLANCHER_MM') / capitaleParPt;
|
||
const paliersNom = paliers(corpsNomMax, corpsNomPlancher);
|
||
function choisirNom() {
|
||
for (const motsEntiers of [true, false]) {
|
||
for (let nombre = 1; nombre <= 3; nombre += 1) {
|
||
const premier = paliersNom.findIndex((corps) => replier(textes.nom, corps, true, nombre, motsEntiers) !== null);
|
||
if (premier < 0) continue;
|
||
const formes = [];
|
||
for (const corps of paliersNom.slice(premier)) {
|
||
const lignes = replier(textes.nom, corps, true, nombre, motsEntiers);
|
||
if (lignes !== null) formes.push({ corps, lignes });
|
||
}
|
||
return { formes, auDela: false };
|
||
}
|
||
}
|
||
const empilables = Math.floor((H + TOLERANCE_MM) / pas(corpsNomPlancher).hauteur);
|
||
const lignes =
|
||
replier(textes.nom, corpsNomPlancher, true, empilables, true) ??
|
||
replier(textes.nom, corpsNomPlancher, true, empilables, false);
|
||
return lignes === null ? null : { formes: [{ corps: corpsNomPlancher, lignes }], auDela: true };
|
||
}
|
||
const refusNom = { refus: { code: 'PLANCHE_NOM_TROP_LONG', details: {} } };
|
||
const nom = choisirNom();
|
||
if (nom === null) return refusNom;
|
||
|
||
const piedReplie = (lignes, corps) => {
|
||
const resultat = [];
|
||
for (const ligne of lignes) {
|
||
const repliee = replier(ligne, corps, false, Infinity);
|
||
if (repliee === null) return null;
|
||
resultat.push(...repliee);
|
||
}
|
||
return resultat;
|
||
};
|
||
const centre = (texte, corps, gras) => (L - largeur(texte, corps, gras)) / 2;
|
||
|
||
// Le badge sous une forme du nom, avec ou sans le nom de l'événement et
|
||
// l'appartenance ; null quand aucune disposition des tours n'y tient.
|
||
function composer(forme, { evenement, avecAppartenance }) {
|
||
const auPlusNom = (corps) => Math.min(corps, forme.corps);
|
||
const corpsAppartenance = auPlusNom(constante('CORPS_APPARTENANCE_PT'));
|
||
const corpsPied = auPlusNom(constante('CORPS_PIED_PT'));
|
||
const corpsTours = paliers(auPlusNom(constante('CORPS_TOURS_PT')), auPlusNom(constante('CORPS_TOURS_PLANCHER_PT')));
|
||
const plancherTours = corpsTours.at(-1);
|
||
|
||
const appartenance =
|
||
!avecAppartenance || textes.appartenance === null || textes.appartenance.trim() === ''
|
||
? null
|
||
: abreger(textes.appartenance, corpsAppartenance, false, L);
|
||
const pied = piedReplie(evenement ? textes.pied : textes.piedReduit, corpsPied);
|
||
if (pied === null) return null;
|
||
|
||
// Les lignes de tour d'une disposition, avec leur abscisse et leur rang ;
|
||
// null quand l'une ne tient pas dans sa colonne.
|
||
function disposerTours({ colonnes, corps }) {
|
||
const poser = (ligne, largeurMax) =>
|
||
corps === plancherTours ? abregerTitre(ligne, corps, largeurMax) : tient(ligne, corps, false, largeurMax) ? ligne : null;
|
||
const poserColonne = (colonne, largeurMax) => {
|
||
const posees = colonne.map((ligne) => poser(ligne, largeurMax));
|
||
return posees.includes(null) ? null : posees;
|
||
};
|
||
const largeurColonne = (colonne) => Math.max(0, ...colonne.map((t) => largeur(t, corps, false)));
|
||
const lignes = textes.lignes;
|
||
if (colonnes === 1) {
|
||
const posees = poserColonne(lignes, L);
|
||
if (posees === null) return null;
|
||
const x = (L - largeurColonne(posees)) / 2;
|
||
return { lignes: posees.map((texte, rang) => ({ texte, x, rang })) };
|
||
}
|
||
// La colonne de gauche reçoit ce que la droite, entière, lui laisse, et
|
||
// jamais moins d'une demi-colonne ; la droite, ce que la gauche posée
|
||
// lui laisse. Les deux tiennent ensemble par construction, et des
|
||
// lignes entières qui tiennent ensemble passent telles quelles.
|
||
const rangees = Math.ceil(lignes.length / 2);
|
||
const gouttiere = corps * MM_PAR_PT;
|
||
const demi = (L - gouttiere) / 2;
|
||
const droiteEntiere = lignes.slice(rangees);
|
||
const gauche = poserColonne(lignes.slice(0, rangees), L - gouttiere - Math.min(largeurColonne(droiteEntiere), demi));
|
||
if (gauche === null) return null;
|
||
const droite = poserColonne(droiteEntiere, L - gouttiere - largeurColonne(gauche));
|
||
if (droite === null) return null;
|
||
const x = (L - (largeurColonne(gauche) + gouttiere + largeurColonne(droite))) / 2;
|
||
const xDroite = x + largeurColonne(gauche) + gouttiere;
|
||
return {
|
||
lignes: [
|
||
...gauche.map((texte, rang) => ({ texte, x, rang })),
|
||
...droite.map((texte, rang) => ({ texte, x: xDroite, rang })),
|
||
],
|
||
};
|
||
}
|
||
|
||
const pasPied = pas(corpsPied);
|
||
const hautDuPied = H - pied.length * pasPied.hauteur;
|
||
// Le flux du haut, jusqu'aux tours.
|
||
const blocs = [];
|
||
let curseur = 0;
|
||
const pasNom = pas(forme.corps);
|
||
for (const texte of forme.lignes) {
|
||
const y = curseur + pasNom.ascendante;
|
||
blocs.push({ texte, corps: forme.corps, gras: true, x: centre(texte, forme.corps, true), y });
|
||
curseur = y + pasNom.descendante;
|
||
}
|
||
if (appartenance !== null) {
|
||
const pasAppartenance = pas(corpsAppartenance);
|
||
const y = curseur + pasAppartenance.ascendante;
|
||
blocs.push({ texte: appartenance, corps: corpsAppartenance, gras: false, x: centre(appartenance, corpsAppartenance, false), y });
|
||
curseur = y + pasAppartenance.descendante;
|
||
}
|
||
// Les dispositions des tours, dans l'ordre de préférence : une colonne
|
||
// du corps des tours au plancher, puis deux.
|
||
const dispositions = [
|
||
...corpsTours.map((corps) => ({ colonnes: 1, corps })),
|
||
...corpsTours.map((corps) => ({ colonnes: 2, corps })),
|
||
];
|
||
for (const disposition of dispositions) {
|
||
const pasTours = pas(disposition.corps);
|
||
if (curseur + Math.ceil(textes.lignes.length / disposition.colonnes) * pasTours.hauteur > hautDuPied + TOLERANCE_MM) {
|
||
continue;
|
||
}
|
||
const tours = disposerTours(disposition);
|
||
if (tours === null) continue;
|
||
const blocsTours = tours.lignes.map(({ texte, x, rang }) => ({
|
||
texte,
|
||
corps: disposition.corps,
|
||
gras: false,
|
||
x,
|
||
y: curseur + rang * pasTours.hauteur + pasTours.ascendante,
|
||
}));
|
||
const blocsPied = pied.map((texte, rang) => ({
|
||
texte,
|
||
corps: corpsPied,
|
||
gras: false,
|
||
x: centre(texte, corpsPied, false),
|
||
y: hautDuPied + rang * pasPied.hauteur + pasPied.ascendante,
|
||
}));
|
||
return { blocs: [...blocs, ...blocsTours, ...blocsPied], colonnesTours: disposition.colonnes };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Les essais, du badge complet au plus dépouillé : chaque forme du nom
|
||
// avec le badge entier, puis le nom au plancher sans le nom de
|
||
// l'événement, puis sans l'appartenance. Un nom au-delà de trois lignes a
|
||
// déjà perdu le nom de l'événement.
|
||
const plancherNom = nom.formes.at(-1);
|
||
const essais = [
|
||
...(nom.auDela ? [] : nom.formes.map((forme) => ({ forme, evenement: true, avecAppartenance: true }))),
|
||
{ forme: plancherNom, evenement: false, avecAppartenance: true },
|
||
{ forme: plancherNom, evenement: false, avecAppartenance: false },
|
||
];
|
||
for (const { forme, ...niveau } of essais) {
|
||
const page = composer(forme, niveau);
|
||
if (page !== null) return page;
|
||
}
|
||
if (nom.auDela) return refusNom;
|
||
return { refus: { code: 'PLANCHE_TROP_DE_TOURS', details: { tours: textes.lignes.length } } };
|
||
}
|