diff --git a/doc/captures/22-plan-brouillon.png b/doc/captures/22-plan-brouillon.png index 4377303..db9c74c 100644 Binary files a/doc/captures/22-plan-brouillon.png and b/doc/captures/22-plan-brouillon.png differ diff --git a/doc/captures/23-plan-retenu.png b/doc/captures/23-plan-retenu.png index 2070606..9ebe610 100644 Binary files a/doc/captures/23-plan-retenu.png and b/doc/captures/23-plan-retenu.png differ diff --git a/doc/captures/manifeste.json b/doc/captures/manifeste.json index 6cbf5fd..4388e87 100644 --- a/doc/captures/manifeste.json +++ b/doc/captures/manifeste.json @@ -17,8 +17,8 @@ {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"a2f41502a104dd60c5f492c964003a8c44419f6fb9453f8e50c1c934e48c2f6e","etape":"9","etat":"57d0ccc945de5920a86bb8d5ded2dbc38a74bdac28324bb130db867677c0d624","facteurEchelle":1,"fichier":"11-propositions","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"7e0d73294910ac8c344f871d1f6e217352a70978b557f80b6516c13049278af5"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":14.26,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":14.26,"role":"texte","seuil":4.5},{"jeton":"--couleur-cadre-ecriture","ratio":4.78,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":15.1,"role":"texte","seuil":4.5}],"sombre":[{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-cadre-ecriture","ratio":9.1,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":14.62,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Revenir en lecture","Écriture","Défaire","Soirée d’essai","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Import","Participants","Tables et tours","Plan","Diagnostic","Génération","Propositions","Badges","Propositions","Ordre du classement, chaque critère au plus faible d'abord :","écart au plafond a priori",", puis","excédent de collisions",", puis","collisions cumulées",", puis","rencontres répétées",", puis","redondance d'appartenance",", puis","l'identifiant croissant",".","Propositions classées","Rang","Proposition","Personnes distinctes rencontrées, minimum : tous","Personnes distinctes rencontrées, minimum : mobiles","Personnes distinctes rencontrées, minimum : ancrés","Collisions cumulées (paire et tour)","Paires distinctes de même appartenance","Excédent de collisions (retrouvailles au-delà de la première)","Rencontres répétées choisies (paires)","Rencontres d'une même paire, au plus","Retours choisis à une table déjà occupée","Appartenances rencontrées A(p), minimum","Redondance d'appartenance, somme Σ r(p)","Taux de diversité d(p), minimum","Plus grand manque (personnes)","Personnes à qui il en manque","Écart d'itinéraire maximal (personnes)","Écart au plafond a priori maximal (personnes)","Certificat","Imposé par les réservations","Commandes","Placement retenu","3","Tel que la proposition n° 3.","11","11","11","26","15","11","30","3","11","3","85","0,27","0","0","0","0","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","1","3","retenue","11","11","11","26","15","11","30","3","11","3","85","0,27","0","0","0","0","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Retenir","2","1","11","11","11","27","15","12","19","3","16","3","85","0,27","0","0","0","0","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Retenir","3","2","11","11","11","33","15","18","54","3","16","3","85","0,27","0","0","0","0","minimum atteint","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Retenir","4","5","10","10","10","20","15","5","22","3","12","3","83","0,27","1","1","1","1","atteignabilité inconnue","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Retenir","5","4","10","10","11","31","14","17","46","3","17","3","82","0,30","1","4","0","1","atteignabilité inconnue","2 retours imposés par 1 ancrage.","Aucune rencontre répétée imposée.","Voir sur le plan","Retenir","Le numéro de la proposition, donné à sa génération ; un numéro effacé ne revient jamais. Il sert de dernier départage : deux propositions égales sur tous les critères se rangent par numéro croissant.","Cible de la somme de redondance","La liste des événements montre, pour le placement retenu, la différence sans signe entre cette cible et la somme Σ r(p) de la colonne « Redondance d'appartenance ». Un nombre entier, 0 ou plus.","Effacer les propositions"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"f81b312de09cfd21e9799849f73d4401c2374bbfbeece4cd6540e2b74af0d57d","etape":"11","etat":"57d0ccc945de5920a86bb8d5ded2dbc38a74bdac28324bb130db867677c0d624","facteurEchelle":1,"fichier":"19-ajustement","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"064c2f47d01eeba5bbd82816965df9fe1ec9e89bd563cfea9321e0225877dd3e"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":14.26,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-cadre-ecriture","ratio":4.78,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-pleine","ratio":15.38,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":7.58,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":15.1,"role":"marque","seuil":3},{"jeton":"--couleur-fantome","ratio":4.34,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":15.1,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-poignee","ratio":7.58,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":7.58,"role":"marque","seuil":3}],"sombre":[{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-cadre-ecriture","ratio":9.1,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-pleine","ratio":14.94,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.89,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":14.62,"role":"marque","seuil":3},{"jeton":"--couleur-fantome","ratio":4.1,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":15.21,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.5,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":11.39,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":11.62,"role":"marque","seuil":3},{"jeton":"--couleur-pastille-texte","ratio":14.51,"role":"texte","seuil":4.5},{"jeton":"--couleur-poignee","ratio":11.32,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":5.59,"role":"marque","seuil":3}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Revenir en lecture","Écriture","Défaire","Soirée d’essai","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Import","Participants","Tables et tours","Plan","Diagnostic","Génération","Propositions","Badges","Plan","Placement retenu","Tour affiché","1","2","3","Zoom +","Zoom −","Ajuster","Listes de noms","Légende","Plein écran","9","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","2.","Gaspard Marbrelle","3.","Ysaline Sablonnet","4.","Anselme Givrecœur","5.","Côme Fenouillet","6.","Apolline Coudraie","7.","Brunœil","8.","Faustine Ormelin","9.","Edgar Ardoisier","10.","Bertille Cressonnier","1.","Basile Lanterneau","— titre","animation","non pourvu","2. (réservée, tour 1)","Capucine Pommerel","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","Gaspard Marbrelle","déplace dans le placement retenu","1","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","Diagnostic","Chœur de l’Anse","4 personnes : au moins 6 collisions cumulées sur les 3 tours, portées par au moins 2 paires distinctes","Société des Hérons","4 personnes : au moins 6 collisions cumulées sur les 3 tours, portées par au moins 2 paires distinctes","Cercle des Saules","3 personnes : au moins 3 collisions cumulées sur les 3 tours, portées par au moins 1 paire distincte","au moins 8 appartenances recroisées par personne","plafond a priori 11 rencontres pour 1 ancré et 11 mobiles","2 retours imposés par 1 ancrage","Listes de travail","Titres de place non pourvus","table 2 : titre","animation","non pourvu","Titres pressentis sans réservation","Gaspard Marbrelle","— titre pressenti","animation","Réserve","Personnes en attente au tour 1","Personne en attente à ce tour."],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"1e58ff6f61c457c8a2b80606ced5ff9f07f7d5dadb5e8e95e77df40c4ae2ae73","etape":"11","etat":"011f06a5e584c50389bf75e8cfcb9eb6c90e8fb488e80a76227f08c9faf5f3a0","facteurEchelle":1,"fichier":"20-mode-lecture","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"0d66820beae96b1724c177ed6a2b949173051c8dec9c564692461aac01877c1b"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":14.26,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-cadre-lecture","ratio":5.29,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-pleine","ratio":15.38,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":7.58,"role":"marque","seuil":3},{"jeton":"--couleur-conflit-appartenance","ratio":8.92,"role":"marque","seuil":3},{"jeton":"--couleur-contour-fort","ratio":12.54,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":15.1,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":15.1,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-poignee","ratio":7.58,"role":"marque","seuil":3},{"jeton":"--couleur-selection","ratio":4.66,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":7.58,"role":"marque","seuil":3}],"sombre":[{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-cadre-lecture","ratio":7.34,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-pleine","ratio":14.94,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.89,"role":"marque","seuil":3},{"jeton":"--couleur-conflit-appartenance","ratio":6.81,"role":"marque","seuil":3},{"jeton":"--couleur-contour-fort","ratio":11.62,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":14.62,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":15.21,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.5,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":14.62,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":11.62,"role":"marque","seuil":3},{"jeton":"--couleur-pastille-texte","ratio":14.51,"role":"texte","seuil":4.5},{"jeton":"--couleur-poignee","ratio":11.32,"role":"marque","seuil":3},{"jeton":"--couleur-selection","ratio":4.18,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":5.59,"role":"marque","seuil":3}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Lecture","Liste des événements","Clair","Sombre","À propos","Modifier","Lecture","Défaire","Soirée d’essai","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Import","Participants","Tables et tours","Plan","Diagnostic","Génération","Propositions","Badges","Plan","Placement retenu","Tour affiché","1","2","3","Zoom +","Zoom −","Ajuster","Listes de noms","Légende","Plein écran","7","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","2.","Ysaline Sablonnet","3.","Anselme Givrecœur","4.","Côme Fenouillet","5.","Apolline Coudraie","6.","Brunœil","7.","Faustine Ormelin","8.","Edgar Ardoisier","9.","Bertille Cressonnier","10. (libre)","1.","Gaspard Marbrelle","— titre","animation","non pourvu","2. (réservée, tour 1)","Capucine Pommerel","3.","Basile Lanterneau","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","Table 2, siège 4","L'événement est ouvert en lecture : « Modifier » permet de le changer.","Modifier","Fermer","1","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","Diagnostic","Chœur de l’Anse","4 personnes : au moins 6 collisions cumulées sur les 3 tours, portées par au moins 2 paires distinctes","Société des Hérons","4 personnes : au moins 6 collisions cumulées sur les 3 tours, portées par au moins 2 paires distinctes","Cercle des Saules","3 personnes : au moins 3 collisions cumulées sur les 3 tours, portées par au moins 1 paire distincte","au moins 8 appartenances recroisées par personne","plafond a priori 11 rencontres pour 1 ancré et 11 mobiles","2 retours imposés par 1 ancrage","Listes de travail","Titres de place non pourvus","table 2 : titre","animation","non pourvu","Titres pressentis sans réservation","Gaspard Marbrelle","— titre pressenti","animation","Réserve","Personnes en attente au tour 1","Personne en attente à ce tour.","Table 2, siège 4","4. (libre)","Choisissez d'abord une personne : dans la réserve, dans la liste des pressentis, ou par « Déplacer » dans son menu.","Portée","Tour 1","Tous les tours","Libellé du titre","Assigner","Réserver","Donner un titre","Désigner"],"theme":"clair","versionReelle":"2026.10.05.01"}, -{"cadre":{"hauteur":700,"largeur":1366},"empreinte":"5e26088ad87296bbfbb8f6b06dd67f165f1fb927624176f50fca320302aa3aa3","etape":"12","etat":"57d0ccc945de5920a86bb8d5ded2dbc38a74bdac28324bb130db867677c0d624","facteurEchelle":1,"fichier":"22-plan-brouillon","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"61ec89a98ba17914c825634660cd43aa6ce4e65e464f9052487a513585a17e0c"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":14.26,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":16.18,"role":"marque","seuil":3},{"jeton":"--couleur-filigrane","ratio":5.1,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":16.18,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":8.13,"role":"marque","seuil":3}],"sombre":[{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":16.18,"role":"marque","seuil":3},{"jeton":"--couleur-filigrane","ratio":5.1,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-papier","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"rgb(28, 33, 40)","ratio":16.18,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Aperçu avant impression","Écriture","Plan de salle","Liste d'accueil","Feuilles par table","Tour imprimé","Tour 1","Tour 2","Tour 3","Listes de noms","Pages du plan","Découpage proposé","Tout sur une page","Découper en pages","Papier :","A4","Changer","Imprimer","Fermer l'aperçu","Page 888 sur 888 — colonne WW, rangée 888","au-dessus : page 888","à droite : page 888","au-dessous : page 888","à gauche : page 888","500","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","listes de noms hors d'échelle — le plan à l'échelle du bâtiment s'imprime listes masquées","Soirée d’essai","Date non fixée","Tour 1","Placement du","6 octobre 2026, 19 h 00","Version","construction de documentation","6","1","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","2.","Ysaline Sablonnet","3.","Anselme Givrecœur","4.","Côme Fenouillet","5.","Apolline Coudraie","6.","Brunœil","7.","Faustine Ormelin","8.","Edgar Ardoisier","9. (libre)","10. (libre)","1.","Gaspard Marbrelle","— titre","animation","non pourvu","2. (réservée, tour 1)","Capucine Pommerel","3.","Basile Lanterneau","4.","Bertille Cressonnier","5. (libre)","6. (libre)","7. (libre)","8. (libre)","Page 1 sur 1 — colonne A, rangée 1","2","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","listes de noms hors d'échelle — le plan à l'échelle du bâtiment s'imprime listes masquées","Soirée d’essai","Date non fixée","Tour 1","Placement du","6 octobre 2026, 19 h 00","Version","construction de documentation","BROUILLON"],"theme":"clair","versionReelle":"2026.10.05.01"}, -{"cadre":{"hauteur":700,"largeur":1366},"empreinte":"c8901face8aa9f7ba6bcaa9d1251bbf340a268b0da6ab5c88ce0a114eb3451bd","etape":"12","etat":"57d0ccc945de5920a86bb8d5ded2dbc38a74bdac28324bb130db867677c0d624","facteurEchelle":1,"fichier":"23-plan-retenu","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"bf2e36213d7d341099d00b0c24e29846b24f316b5a77fe2d45ff82dd2b6c33d9"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":14.26,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"--couleur-conflit-appartenance","ratio":8.92,"role":"marque","seuil":3},{"jeton":"--couleur-contour-fort","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":16.18,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":16.18,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":8.13,"role":"marque","seuil":3}],"sombre":[{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"--couleur-conflit-appartenance","ratio":8.92,"role":"marque","seuil":3},{"jeton":"--couleur-contour-fort","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":16.18,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-papier","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"rgb(28, 33, 40)","ratio":16.18,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Aperçu avant impression","Écriture","Plan de salle","Liste d'accueil","Feuilles par table","Tour imprimé","Tour 1","Tour 2","Tour 3","Listes de noms","Pages du plan","Découpage proposé","Tout sur une page","Découper en pages","Papier :","A4","Changer","Imprimer","Fermer l'aperçu","Page 888 sur 888 — colonne WW, rangée 888","au-dessus : page 888","à droite : page 888","au-dessous : page 888","à gauche : page 888","500","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","listes de noms hors d'échelle — le plan à l'échelle du bâtiment s'imprime listes masquées","Soirée d’essai","Date non fixée","Tour 1","Placement du","6 octobre 2026, 19 h 02","Version","construction de documentation","9","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","2.","Gaspard Marbrelle","3.","Ysaline Sablonnet","4.","Anselme Givrecœur","5.","Côme Fenouillet","6.","Apolline Coudraie","7.","Brunœil","8.","Faustine Ormelin","9.","Edgar Ardoisier","10.","Bertille Cressonnier","1.","Basile Lanterneau","— titre","animation","non pourvu","2. (réservée, tour 1)","Capucine Pommerel","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","Page 1 sur 1 — colonne A, rangée 1","2","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","listes de noms hors d'échelle — le plan à l'échelle du bâtiment s'imprime listes masquées","Soirée d’essai","Date non fixée","Tour 1","Placement du","6 octobre 2026, 19 h 02","Version","construction de documentation"],"theme":"clair","versionReelle":"2026.10.05.01"}, +{"cadre":{"hauteur":700,"largeur":1366},"empreinte":"5e26088ad87296bbfbb8f6b06dd67f165f1fb927624176f50fca320302aa3aa3","etape":"12","etat":"57d0ccc945de5920a86bb8d5ded2dbc38a74bdac28324bb130db867677c0d624","facteurEchelle":1,"fichier":"22-plan-brouillon","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"192d134eaeb02c2298b31b1d063caff155ff13c4b8db9370d0271cd429e65818"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":14.26,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":16.18,"role":"marque","seuil":3},{"jeton":"--couleur-filigrane","ratio":5.1,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":16.18,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":8.13,"role":"marque","seuil":3}],"sombre":[{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":16.18,"role":"marque","seuil":3},{"jeton":"--couleur-filigrane","ratio":5.1,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-papier","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"rgb(28, 33, 40)","ratio":16.18,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Aperçu avant impression","Écriture","Plan de salle","Liste d'accueil","Feuilles par table","Tour imprimé","Tour 1","Tour 2","Tour 3","Listes de noms","Pages du plan","Découpage proposé","Tout sur une page","Découper en pages","Papier :","A4","Changer","Imprimer","Fermer l'aperçu","Page 888 sur 888 — colonne WW, rangée 888","au-dessus : page 888","à droite : page 888","au-dessous : page 888","à gauche : page 888","500","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","listes de noms hors d'échelle — le plan à l'échelle du bâtiment s'imprime listes masquées","Soirée d’essai","Date non fixée","Tour 1","Placement du","6 octobre 2026, 19 h 00","Version","construction de documentation","6","1","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","2.","Ysaline Sablonnet","3.","Anselme Givrecœur","4.","Côme Fenouillet","5.","Apolline Coudraie","6.","Brunœil","7.","Faustine Ormelin","8.","Edgar Ardoisier","9. (libre)","10. (libre)","1.","Gaspard Marbrelle","— titre","animation","non pourvu","2. (réservée, tour 1)","Capucine Pommerel","3.","Basile Lanterneau","4.","Bertille Cressonnier","5. (libre)","6. (libre)","7. (libre)","8. (libre)","Page 1 sur 1 — colonne A, rangée 1","2","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","listes de noms hors d'échelle — le plan à l'échelle du bâtiment s'imprime listes masquées","Soirée d’essai","Date non fixée","Tour 1","Placement du","6 octobre 2026, 19 h 00","Version","construction de documentation","BROUILLON"],"theme":"clair","versionReelle":"2026.10.05.01"}, +{"cadre":{"hauteur":700,"largeur":1366},"empreinte":"c8901face8aa9f7ba6bcaa9d1251bbf340a268b0da6ab5c88ce0a114eb3451bd","etape":"12","etat":"57d0ccc945de5920a86bb8d5ded2dbc38a74bdac28324bb130db867677c0d624","facteurEchelle":1,"fichier":"23-plan-retenu","graine":4242,"image":{"hauteur":700,"largeur":1366,"sha256":"f3cc86d1617448b037bc00ac40c81b590cf98b1c81966f75b47635a545898302"},"instant":"2026-10-06T19:02:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-bandeau-fond","ratio":14.26,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"--couleur-conflit-appartenance","ratio":8.92,"role":"marque","seuil":3},{"jeton":"--couleur-contour-fort","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":16.18,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":16.18,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":8.13,"role":"marque","seuil":3}],"sombre":[{"jeton":"--couleur-bandeau-fond","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-bandeau-texte","ratio":12.84,"role":"texte","seuil":4.5},{"jeton":"--couleur-chaise-pleine","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-chaise-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"--couleur-conflit-appartenance","ratio":8.92,"role":"marque","seuil":3},{"jeton":"--couleur-contour-fort","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":16.18,"role":"marque","seuil":3},{"jeton":"--couleur-hachure","ratio":16.48,"role":"marque","seuil":3},{"jeton":"--couleur-marque","ratio":3.63,"role":"marque","seuil":3},{"jeton":"--couleur-papier","ratio":13.44,"role":"texte","seuil":4.5},{"jeton":"--couleur-pastille-fond","ratio":13.44,"role":"marque","seuil":3},{"jeton":"--couleur-table-trait","ratio":8.13,"role":"marque","seuil":3},{"jeton":"rgb(28, 33, 40)","ratio":16.18,"role":"texte","seuil":4.5}]},"texte":["Gestion table tournante Libre","construction de documentation","documentation","Écriture","Liste des événements","Clair","Sombre","À propos","Aperçu avant impression","Écriture","Plan de salle","Liste d'accueil","Feuilles par table","Tour imprimé","Tour 1","Tour 2","Tour 3","Listes de noms","Pages du plan","Découpage proposé","Tout sur une page","Découper en pages","Papier :","A4","Changer","Imprimer","Fermer l'aperçu","Page 888 sur 888 — colonne WW, rangée 888","au-dessus : page 888","à droite : page 888","au-dessous : page 888","à gauche : page 888","500","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","listes de noms hors d'échelle — le plan à l'échelle du bâtiment s'imprime listes masquées","Soirée d’essai","Date non fixée","Tour 1","Placement du","6 octobre 2026, 19 h 02","Version","construction de documentation","9","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","2.","Gaspard Marbrelle","3.","Ysaline Sablonnet","4.","Anselme Givrecœur","5.","Côme Fenouillet","6.","Apolline Coudraie","7.","Brunœil","8.","Faustine Ormelin","9.","Edgar Ardoisier","10.","Bertille Cressonnier","1.","Basile Lanterneau","— titre","animation","non pourvu","2. (réservée, tour 1)","Capucine Pommerel","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","Page 1 sur 1 — colonne A, rangée 1","2","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","listes de noms hors d'échelle — le plan à l'échelle du bâtiment s'imprime listes masquées","Soirée d’essai","Date non fixée","Tour 1","Placement du","6 octobre 2026, 19 h 02","Version","construction de documentation"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":1123,"largeur":794},"empreinte":"749c29d0f4251b3f1d25459d2d626ad6910816fdf8e250c2ade168dfe59d3c3a","etape":"12","etat":"57d0ccc945de5920a86bb8d5ded2dbc38a74bdac28324bb130db867677c0d624","facteurEchelle":1,"fichier":"24-badges-controle","graine":4242,"image":{"hauteur":1123,"largeur":794,"sha256":"4731d411c33a6fa9a889acbfe92d1460a988c6f202f96578286b7d0e02109eb9"},"instant":"2026-10-06T19:03:00-04:00","mode":"pdfjs-dist-96ppp","pdf":"2f0f58fece0ba41d5a0825c83f30dea7931bc9b665e3890b4fe4f371d90559f4","source":"pdf","styles":{"clair":[],"sombre":[]},"texte":["0","50","100","Badge : 105 × 148 mm (A6)","Papier : A4, 210 × 297 mm","Imposition : feuille par feuille","Planches : 3","12 badges pour 13 participants, 1 exclu","Événement : Soirée d’essai","Source : le placement retenu","Placement du 6 octobre 2026 à 19:02:00","Version : construction de documentation","Imprimer en recto seul, à 100 %"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":1123,"largeur":794},"empreinte":"632c33170b03303a4d6314e6f3cbaf2da81f335a3a2397d94e90bf7aa202d688","etape":"12","etat":"57d0ccc945de5920a86bb8d5ded2dbc38a74bdac28324bb130db867677c0d624","facteurEchelle":1,"fichier":"25-badges-planche","graine":4242,"image":{"hauteur":1123,"largeur":794,"sha256":"5375de3104896af92144a7b455746f989fc802cc320b8b6f52e1351c7718bf43"},"instant":"2026-10-06T19:03:00-04:00","mode":"pdfjs-dist-96ppp","pdf":"2f0f58fece0ba41d5a0825c83f30dea7931bc9b665e3890b4fe4f371d90559f4","source":"pdf","styles":{"clair":[],"sombre":[]},"texte":["Edgar Ardoisier","Société des Hérons","Tour 1 — table 1","Tour 2 — table 1","Tour 3 — table 2","Soirée d’essai","Placement du 6 octobre 2026 à 19:02:00","Brunœil","Cercle des Saules","Tour 1 — table 1","Tour 2 — table 2","Tour 3 — table 1","Soirée d’essai","Placement du 6 octobre 2026 à 19:02:00","Apolline Coudraie","Chœur de l’Anse","Tour 1 — table 1","Tour 2 — table 2","Tour 3 — table 1","Soirée d’essai","Placement du 6 octobre 2026 à 19:02:00","Bertille Cressonnier","Cercle des Saules","Tour 1 — table 1","Tour 2 — table 2","Tour 3 — table 1","Soirée d’essai","Placement du 6 octobre 2026 à 19:02:00"],"theme":"clair","versionReelle":"2026.10.05.01"} ],"format":1,"pilote":{"cadre":{"hauteur":700,"largeur":1366},"facteurEchelle":1,"mode":"chromium-headless-shell","navigateur":"chromium","versionNavigateur":"153.0.8010.12"},"reportees":[ diff --git a/src/geometrie/pages.js b/src/geometrie/pages.js index ec8a8c0..301867d 100644 --- a/src/geometrie/pages.js +++ b/src/geometrie/pages.js @@ -98,7 +98,9 @@ function bords(n, utileMm, echelle, centre, etendue) { * le plan se lit sur une page : elle est alors d'une page. * * L'orientation est celle qui donne le moins de pages ; à pages égales, la - * plus grande échelle ; à échelle égale aussi, le paysage. + * plus grande échelle ; à échelle égale aussi, le paysage. Une orientation + * sans surface utile ne concourt pas : une bande plus haute que la page + * couchée laisse le portrait seul. * * Les tuiles, toutes de même taille, se rangent par rangées de haut en bas, * de gauche à droite dans chaque rangée ; rang est l'indice dans pages, @@ -130,8 +132,9 @@ function bords(n, utileMm, echelle, centre, etendue) { * voisins: {haut: number|null, droite: number|null, * bas: number|null, gauche: number|null}}, * tables: number[]}>}} - * @throws {RangeError} papier ou mode inconnu ; marge, bande ou corps hors de leur domaine ; page - * sans surface utile ; englobant plat, inversé ou non fini ; table sans hauteur de liste + * @throws {RangeError} papier ou mode inconnu ; marge, bande ou corps hors de leur domaine ; aucune + * orientation avec une surface utile ; englobant plat, inversé ou non fini ; table sans + * hauteur de liste */ export function decouperPlan( englobant, @@ -164,17 +167,23 @@ export function decouperPlan( const seuil = corpsMinMm / corpsDessinCm; const { largeur: petit, hauteur: grand } = PAPIERS[papier]; - // Paysage d'abord : il garde la place à toute égalité. - const candidats = [ + // Paysage d'abord : il garde la place à toute égalité. Une orientation + // sans surface utile — une bande plus haute que la page couchée — n'est + // pas candidate ; le portrait, plus haut, garde alors la sienne. + const orientations = [ { orientation: 'paysage', feuille: { largeur: grand, hauteur: petit } }, { orientation: 'portrait', feuille: { largeur: petit, hauteur: grand } }, - ].map(({ orientation, feuille }) => { - const utile = { largeur: feuille.largeur - 2 * margeMm, hauteur: feuille.hauteur - 2 * margeMm - bandeMm }; - if (!(utile.largeur > 0 && utile.hauteur > 0)) { - throw new RangeError(`page ${orientation} sans surface utile : ${utile.largeur} × ${utile.hauteur} mm`); - } - return { orientation, utile, ...disposer(utile, plan, seuil, mode) }; - }); + ].map(({ orientation, feuille }) => ({ + orientation, + utile: { largeur: feuille.largeur - 2 * margeMm, hauteur: feuille.hauteur - 2 * margeMm - bandeMm }, + })); + const candidats = orientations + .filter(({ utile }) => utile.largeur > 0 && utile.hauteur > 0) + .map(({ orientation, utile }) => ({ orientation, utile, ...disposer(utile, plan, seuil, mode) })); + if (candidats.length === 0) { + const surfaces = orientations.map(({ orientation, utile }) => `${orientation} ${utile.largeur} × ${utile.hauteur} mm`); + throw new RangeError(`aucune orientation n'a de surface utile : ${surfaces.join(', ')}`); + } const retenu = candidats.reduce((meilleur, c) => { const pages = c.colonnes * c.rangees; const pagesMeilleur = meilleur.colonnes * meilleur.rangees; diff --git a/src/geometrie/pages.test.js b/src/geometrie/pages.test.js index 9a0d535..3090c3f 100644 --- a/src/geometrie/pages.test.js +++ b/src/geometrie/pages.test.js @@ -358,8 +358,34 @@ describe('decouperPlan : les refus', () => { assert.throws(() => decouperPlan(ok, { ...OPTIONS, corpsDessinCm: Number.NaN }), RangeError); assert.throws(() => decouperPlan(ok, { ...OPTIONS, margeMm: -1 }), RangeError); // A4 : 210 − 2 × 105 = 0 mm, en largeur du portrait et en hauteur du paysage. - assert.throws(() => decouperPlan(ok, { ...OPTIONS, margeMm: 105 }), RangeError); - assert.throws(() => decouperPlan(ok, { ...OPTIONS, bandeMm: 300 }), RangeError); + assert.throws(() => decouperPlan(ok, { ...OPTIONS, margeMm: 105 }), { name: 'RangeError', message: /aucune orientation/ }); + // 297 − 20 − 300 < 0 : ni le portrait ni le paysage n'ont de hauteur. + assert.throws(() => decouperPlan(ok, { ...OPTIONS, bandeMm: 300 }), { name: 'RangeError', message: /aucune orientation/ }); + // 297 − 20 − 277 = 0 : le portrait n'a plus de hauteur non plus. + assert.throws(() => decouperPlan(ok, { ...OPTIONS, bandeMm: 277 }), { name: 'RangeError', message: /aucune orientation/ }); + }); + + test('une bande plus haute que le paysage : la découpe se replie sur le portrait, qui garde de la place', () => { + // Bande de 195 mm, plus de la moitié d'une page couchée : le paysage a + // 210 − 20 − 195 = −5 mm de hauteur, le portrait 297 − 20 − 195 = 82 mm. + // Le plan large de 100 × 50 cm, qui préférerait le paysage, s'imprime en + // portrait : e₁ = min(190/100, 82/50) = 1,64 < 2, sa grille au seuil est + // ⌈200/190⌉ × ⌈100/82⌉ = 2 × 2, à min(380/100, 164/50) = 3,28. + for (const mode of ['auto', 'unePage', 'decoupe']) { + const plan = decouperPlan(boite(0, 0, 100, 50), { ...OPTIONS, bandeMm: 195, mode }); + assert.equal(plan.orientation, 'portrait', mode); + for (const page of plan.pages) { + proche(page.cadrage.hauteur * plan.echelle, 82, `${mode} : hauteur utile`, 1e-4); + proche(page.cadrage.largeur * plan.echelle, 190, `${mode} : largeur utile`, 1e-4); + } + } + assert.equal(decouperPlan(boite(0, 0, 100, 50), { ...OPTIONS, bandeMm: 195 }).pages.length, 4); + // Lettre : 215,9 − 20 − 196 < 0 en paysage, 279,4 − 20 − 196 = 63,4 en portrait. + const lettre = decouperPlan(boite(0, 0, 100, 50), { ...OPTIONS, papier: 'Lettre', bandeMm: 196, mode: 'unePage' }); + assert.equal(lettre.orientation, 'portrait'); + proche(lettre.pages[0].cadrage.hauteur * lettre.echelle, PAPIERS.Lettre.hauteur - 20 - 196, 'hauteur utile Lettre', 1e-4); + // Le plan vide aussi : une page portrait. + assert.equal(decouperPlan(null, { ...OPTIONS, bandeMm: 195 }).orientation, 'portrait'); }); test('un englobant plat, inversé, non fini, ou hors des cotes représentables', () => { diff --git a/src/interface/impression.css b/src/interface/impression.css index d69531a..5d6fcc9 100644 --- a/src/interface/impression.css +++ b/src/interface/impression.css @@ -65,7 +65,7 @@ [data-impression] [data-feuille] { margin: 0 !important; - border: 0 !important; + outline: none !important; box-shadow: none !important; break-after: page; } diff --git a/src/interface/impression/Feuille.navigateur.test.js b/src/interface/impression/Feuille.navigateur.test.js new file mode 100644 index 0000000..812f677 --- /dev/null +++ b/src/interface/impression/Feuille.navigateur.test.js @@ -0,0 +1,107 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// La feuille de l'aperçu est la page imprimée (§ 11.7 : « ce n'est pas une +// seconde mise en page : c'est la même ») : sa zone de contenu +// [data-contenu] a la même largeur à l'écran et sous les médias +// d'impression émulés, celle de la page moins ses deux marges. Le bord que +// l'écran dessine autour de la feuille ne prend aucune place : la tuile du +// plan, à la largeur utile, y tient sans être rognée, et les listes se +// mesurent et se paginent à l'écran à la largeur du papier. +// +// App est montée par test/navigateur/ecrans.js sur le système de fichiers +// simulé, au cadre de 1 366 × 700. Le projet navigateur emploie expect, et +// test et describe de Vitest (§ 14.8). +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +import { cdp } from 'vitest/browser'; +import { texteLivre } from '../../demo/livrees.js'; +import { constante } from '../../geometrie/constantes.js'; +import { analyser } from '../../stockage/document.js'; +import { fixerCadre } from '../../../test/navigateur/entrees.js'; +import { attendre, chargePrincipale, cliquer, creerContexte, ouvrirApercu } from '../../../test/navigateur/ecrans.js'; +import { MM_PAR_PX } from './modele.js'; + +// Images attendues au plus par étape : bien au-delà d'un rendu, et sous le +// délai d'une épreuve, pour qu'une étape qui ne vient pas se nomme. +const IMAGES = 300; + +// Une feuille se mesure au dixième de millimètre (§ 11.7). +const TOLERANCE_MM = 0.1; + +let contexte = null; +beforeEach(async () => { + await fixerCadre(1366, 700); + contexte = creerContexte(); +}); +afterEach(async () => { + const erreurs = contexte.erreurs(); + await contexte.defaire(); + expect(erreurs, 'erreurs reçues par le collecteur').toEqual([]); +}); + +const feuilles = (m) => [...m.racine.querySelectorAll('[data-impression] [data-feuille]')]; + +// Chaque feuille du document : sa largeur et celle de sa zone de contenu, +// en px CSS, ce que le contenu porte au-delà de sa largeur visible, et le +// bord tracé autour de la feuille — le style de son contour, s'il tient +// dans le rectangle de la feuille (décalage + épaisseur ≤ 0), son ombre. +const mesures = (m) => + feuilles(m).map((feuille) => { + const contenu = feuille.querySelector('[data-contenu]'); + const style = getComputedStyle(feuille); + return { + rang: feuille.dataset.feuille, + feuille: feuille.getBoundingClientRect().width, + contenu: contenu.getBoundingClientRect().width, + deborde: contenu.scrollWidth - contenu.clientWidth, + bord: style.outlineStyle, + bordDedans: parseFloat(style.outlineOffset) + parseFloat(style.outlineWidth) <= 0, + ombre: style.boxShadow, + }; + }); + +// Les mesures sous les médias d'impression émulés, puis l'écran rétabli. +async function mesuresImprimees(m) { + await cdp().send('Emulation.setEmulatedMedia', { media: 'print' }); + try { + return mesures(m); + } finally { + await cdp().send('Emulation.setEmulatedMedia', { media: '' }); + } +} + +// La zone de contenu de chaque feuille, à l'écran et à l'impression : la +// même largeur, celle de la feuille moins ses deux marges, et rien qui +// déborde. L'écran trace le bord de la feuille ; le papier n'en porte ni +// bord ni ombre. +async function verifierLargeurs(m, cas) { + const marge = constante('MARGE_PAGE_MM'); + const ecran = mesures(m); + const papier = await mesuresImprimees(m); + expect(ecran.length, `${cas} : des feuilles`).toBeGreaterThan(0); + expect(papier.map(({ rang }) => rang), `${cas} : les mêmes feuilles`).toEqual(ecran.map(({ rang }) => rang)); + for (const [i, sur] of ecran.entries()) { + const imprime = papier[i]; + const libelle = `${cas}, feuille ${sur.rang} : écran ${JSON.stringify(sur)}, impression ${JSON.stringify(imprime)}`; + expect(Math.abs(sur.contenu - imprime.contenu) * MM_PAR_PX, libelle).toBeLessThanOrEqual(TOLERANCE_MM); + expect(Math.abs((sur.feuille - sur.contenu) * MM_PAR_PX - 2 * marge), libelle).toBeLessThanOrEqual(TOLERANCE_MM); + expect(sur.deborde, libelle).toBeLessThanOrEqual(0); + expect([sur.bord, sur.bordDedans], libelle).toEqual(['solid', true]); + expect([imprime.bord, imprime.ombre], libelle).toEqual(['none', 'none']); + } +} + +describe('la feuille à l’écran est la feuille imprimée (§ 11.7)', () => { + test('le plan de la principale : la zone de contenu a la même largeur à l’écran et à l’impression, et la tuile y tient', async () => { + const m = await ouvrirApercu(contexte, chargePrincipale()); + await verifierLargeurs(m, 'plan, A4'); + }); + + test('la grande démonstration sur papier Lettre, plan puis liste d’accueil : la même largeur sur chaque feuille', async () => { + const m = await ouvrirApercu(contexte, analyser(texteLivre('grande')).charge, { papier: 'Lettre' }); + await verifierLargeurs(m, 'plan, Lettre'); + await cliquer(m, '[data-ecran="apercu"] [data-commande="vue"][data-cible="accueil"]'); + await attendre('la liste d’accueil', () => m.racine.querySelector('[data-impression][data-vue-impression="accueil"] [data-feuille]'), IMAGES); + await verifierLargeurs(m, 'accueil, Lettre'); + }); +}); diff --git a/src/interface/impression/Feuille.svelte b/src/interface/impression/Feuille.svelte index 9bf63c9..955589f 100644 --- a/src/interface/impression/Feuille.svelte +++ b/src/interface/impression/Feuille.svelte @@ -13,6 +13,11 @@ // [data-impression], qui porte les jetons du clair, et peint son fond du // papier et son texte de l'encre de la page. // + // À l'écran, le bord de la feuille est un contour tracé sur son propre + // rectangle, qui ne prend aucune place : la zone de contenu a la largeur + // de la page moins ses marges, à l'écran comme sur le papier, et la tuile + // du plan, à la largeur utile, y tient sans être rognée. + // // La zone de contenu mesure la hauteur utile que la découpe reçoit : la // page, moins les marges, moins bandeMm, la bande mesurée sur la feuille // gabarit. Sans bandeMm, la feuille est le gabarit : sa hauteur suit ce @@ -61,7 +66,8 @@ flex-direction: column; margin: 0 auto var(--espace-5); overflow: hidden; - border: 1px solid var(--couleur-page-filet); + outline: 1px solid var(--couleur-page-filet); + outline-offset: -1px; background-color: var(--couleur-papier); color: var(--couleur-page-texte); box-shadow: var(--ombre-2); diff --git a/src/interface/impression/modele.js b/src/interface/impression/modele.js index 09de4b9..fd2ef5f 100644 --- a/src/interface/impression/modele.js +++ b/src/interface/impression/modele.js @@ -5,14 +5,12 @@ // ce qu'une vue imprime — papier, orientation, tour —, les dimensions d'une // page et de sa zone de contenu, la bande mesurée, la page nommée que la // feuille d'impression déclare pour chaque papier et chaque orientation, le -// pied de cinq éléments et le filigrane d'une feuille, la source que -// l'aperçu imprime et la découpe du plan. Le modèle ne rend que des -// nombres, des codes, des clés et des données ; les composants les -// traduisent et les posent. +// pied de cinq éléments et le filigrane d'une feuille, et la source que +// l'aperçu imprime. La découpe du plan en pages n'est pas ici : pagesDuPlan +// (plan.js) l'appelle seule. Le modèle ne rend que des nombres, des codes, +// des clés et des données ; les composants les traduisent et les posent. import { etatPlaces } from '../../application/places.js'; -import { decouperPlan } from '../../geometrie/pages.js'; import { PAPIERS } from '../../geometrie/papiers.js'; -import { englobantImpression, geometrieTable } from '../../geometrie/tables.js'; import { DECOUPES, VUES_IMPRESSION } from '../etat_ui.js'; /** Millimètres par pixel CSS : 96 px au pouce, 25,4 mm au pouce. */ @@ -243,16 +241,3 @@ export function sortieApercu(charge, { tour }, proposition) { tour: tour === 'tous' ? 'tous' : tourEtat, }; } - -/** - * La découpe du plan d'un tour en pages (decouperPlan, § 11.7) : les - * géométries des tables de l'état des places, à leur capacité, leur - * englobant, et la bande mesurée sur la feuille gabarit. - * - * @param {Object} etat EtatPlaces - * @param {{papier: string, bandeMm: number, decoupe: string}} options - */ -export function decoupeDuPlan(etat, { papier, bandeMm, decoupe }) { - const geometries = etat.tables.map((table) => geometrieTable(table, table.capacite)); - return decouperPlan(englobantImpression(geometries), { papier, bandeMm, mode: decoupe, geometries }); -} diff --git a/src/interface/impression/modele.test.js b/src/interface/impression/modele.test.js index 5860a1b..933b4ed 100644 --- a/src/interface/impression/modele.test.js +++ b/src/interface/impression/modele.test.js @@ -12,9 +12,7 @@ import { TABLES_APPLICATION } from '../../application/libelles.js'; import { etatPlaces } from '../../application/places.js'; import { MARQUE_TEMOIN, creerTraduction } from '../../application/traduction.js'; import { constante } from '../../geometrie/constantes.js'; -import { decouperPlan } from '../../geometrie/pages.js'; import { PAPIERS } from '../../geometrie/papiers.js'; -import { englobantImpression, geometrieTable } from '../../geometrie/tables.js'; import { texteLivre } from '../../demo/livrees.js'; import { analyser } from '../../stockage/document.js'; import { TABLES_INTERFACE } from '../libelles.js'; @@ -25,7 +23,6 @@ import { clesPapier, cleDecoupe, cleVue, - decoupeDuPlan, dimensionsPage, feuilleDe, filigraneDe, @@ -220,7 +217,7 @@ describe('le filigrane BROUILLON (§ 11.7, § 14.5)', () => { }); }); -describe('la sortie de l’aperçu et la découpe du plan', () => { +describe('la sortie de l’aperçu', () => { test('sans placement, la source des réservations, sans proposition ; le tour du plan borné aux tours de la soirée', () => { const charge = petite(); const sortie = sortieApercu(charge, { tour: 9 }, null); @@ -235,17 +232,16 @@ describe('la sortie de l’aperçu et la découpe du plan', () => { const sortie = sortieApercu(avecProposition(), { tour: 1 }, 1); assert.deepEqual([sortie.source, sortie.proposition, sortie.tour], ['proposition', 1, 1]); }); +}); - test('la découpe du plan reçoit la bande mesurée : une bande plus haute réduit la surface utile', () => { - const etat = etatPlaces(petite(), { tour: 1 }); - const geometries = etat.tables.map((table) => geometrieTable(table, table.capacite)); - for (const bandeMm of [0, 12.5]) { - const attendue = decouperPlan(englobantImpression(geometries), { papier: 'A4', bandeMm, mode: 'unePage', geometries }); - assert.deepEqual(decoupeDuPlan(etat, { papier: 'A4', bandeMm, decoupe: 'unePage' }), attendue, String(bandeMm)); - } - const basse = decoupeDuPlan(etat, { papier: 'A4', bandeMm: 0, decoupe: 'unePage' }); - const haute = decoupeDuPlan(etat, { papier: 'A4', bandeMm: 150, decoupe: 'unePage' }); - assert.ok(haute.echelle < basse.echelle, `${haute.echelle} < ${basse.echelle}`); +describe('la découpe du plan : une seule implémentation (§ 13.2)', () => { + test('le modèle de l’aperçu ne découpe pas le plan : la vue du plan passe par pagesDuPlan (plan.js) seule', () => { + // Un second appelant de decouperPlan découperait le même plan sur + // d'autres géométries ou sans les listes, et deux aperçus d'un même + // état ne compteraient plus les mêmes pages. Le texte du module, sans + // ses commentaires, n'importe ni la découpe ni l'englobant d'impression. + const source = readFileSync(new URL('./modele.js', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ''); + assert.doesNotMatch(source, /geometrie\/pages\.js|\bdecouperPlan\b|\benglobantImpression\b/); }); }); diff --git a/src/interface/libelles/plan.js b/src/interface/libelles/plan.js index c6b0609..c8db7ef 100644 --- a/src/interface/libelles/plan.js +++ b/src/interface/libelles/plan.js @@ -99,13 +99,17 @@ export const TABLE = new Map([ // courant modifie : l'état des données, non le geste (§ 5.8) ; une // personne réservée déplace sa réservation, dont il dit la portée, et // au-dessus du sol la retire (« ce tour ») ou se la voit refuser (« tous - // les tours »). Le nom de la personne est une donnée, dans son propre nœud. + // les tours ») ; une personne que rien ne tient n'y change rien ; sous le + // retenu, le sol retire du tour. Le nom de la personne est une donnée, + // dans son propre nœud. ['plan.fantome.reservations', 'pose une réservation'], ['plan.fantome.reservationTour', 'déplace sa réservation de ce tour'], ['plan.fantome.reservationTous', 'déplace sa réservation, à tous les tours'], ['plan.fantome.retireReservationTour', 'retire sa réservation de ce tour'], ['plan.fantome.refuseTous', 'refusé : sa réservation vaut pour tous les tours'], + ['plan.fantome.rienNeChange', 'aucune réservation à retirer : rien ne change'], ['plan.fantome.retenu', 'déplace dans le placement retenu'], + ['plan.fantome.retireDuTour', 'retire du tour : attend dans la réserve'], ['plan.fantome.vueSeule', 'proposition en vue seule : rien ne change'], // La place où un refus ou un avertissement du plan se pose. ['plan.refus.table', ({ numero }) => `Table ${numero}`], diff --git a/src/interface/plan/gestes.js b/src/interface/plan/gestes.js index 9006555..d3c5cfd 100644 --- a/src/interface/plan/gestes.js +++ b/src/interface/plan/gestes.js @@ -231,13 +231,24 @@ export function memoriser(controler) { } // Les clés du libellé du fantôme, par cible de l'état des places : ce que -// le dépôt modifie, ou rien sous une cible null. +// le dépôt sur une chaise modifie, ou rien sous une cible null. const CLES_FANTOME = new Map([ ['reservations', 'plan.fantome.reservations'], ['retenu', 'plan.fantome.retenu'], [null, 'plan.fantome.vueSeule'], ]); +// Les mêmes, au-dessus du sol (commandeDeDepot, depot.js) : sous le retenu, +// le dépôt retire la personne du tour (retirerDuTour) ; sous les +// réservations, une personne qu'aucune réservation ne tient n'a rien à +// retirer, et le dépôt ne rend rien ; sous une cible null, le refus de la +// vue seule, comme sur une chaise. +const CLES_FANTOME_SUR_LE_SOL = new Map([ + ['reservations', 'plan.fantome.rienNeChange'], + ['retenu', 'plan.fantome.retireDuTour'], + [null, 'plan.fantome.vueSeule'], +]); + // Sous la cible 'reservations', la clé d'une personne que sa réservation // tient à sa chaise, par portée : son dépôt sur une chaise est // deplacerReservation, qui garde la portée. @@ -257,13 +268,18 @@ const CLES_RESERVATION_SUR_LE_SOL = new Map([ /** * La clé du libellé que le fantôme porte pendant le glissement d'une * personne (§ 5.8, § 7.1), qui dit ce que le relâcher au point courant - * modifie : sous la cible 'reservations', « pose une réservation » pour une - * personne que rien ne tient, sinon le déplacement de sa réservation et sa - * portée — « tous les tours » s'y dit — ; au-dessus du sol, la même - * réservation se dit retirée (« ce tour ») ou refusée (« tous les tours ») ; - * « déplace dans le placement retenu » sous 'retenu' ; la vue seule sous une - * cible null. Une cible, une portée ou un sol hors du contrat lève : il ne - * prend aucun libellé par défaut. + * modifie, celui que rend commandeDeDepot (depot.js) : + * - sous la cible 'reservations', sur une chaise, « pose une réservation » + * pour une personne que rien ne tient, sinon le déplacement de sa + * réservation et sa portée — « tous les tours » s'y dit ; au-dessus du + * sol, la même réservation se dit retirée (« ce tour ») ou refusée + * (« tous les tours »), et « rien ne change » pour une personne que rien + * ne tient ; + * - sous 'retenu', « déplace dans le placement retenu » sur une chaise, + * « retire du tour » au-dessus du sol ; + * - la vue seule sous une cible null, partout. + * Une cible, une portée ou un sol hors du contrat lève : il ne prend aucun + * libellé par défaut. * * @param {'reservations'|'retenu'|null} cible EtatPlaces.cible * @param {'tous'|'tour'|null} [portee] la portée de la réservation qui tient @@ -277,7 +293,7 @@ export function cleFantome(cible, portee = null, surLeSol = false) { throw new TypeError(`portée du fantôme hors du contrat : ${JSON.stringify(portee)}`); } if (typeof surLeSol !== 'boolean') throw new TypeError(`sol du fantôme hors du contrat : ${JSON.stringify(surLeSol)}`); - if (cible !== 'reservations' || portee === null) return CLES_FANTOME.get(cible); + if (cible !== 'reservations' || portee === null) return (surLeSol ? CLES_FANTOME_SUR_LE_SOL : CLES_FANTOME).get(cible); return (surLeSol ? CLES_RESERVATION_SUR_LE_SOL : CLES_RESERVATION_TENUE).get(portee); } diff --git a/src/interface/plan/gestes.test.js b/src/interface/plan/gestes.test.js index 1c41bce..06d7be4 100644 --- a/src/interface/plan/gestes.test.js +++ b/src/interface/plan/gestes.test.js @@ -15,6 +15,7 @@ import { constante } from '../../geometrie/constantes.js'; import { placesSousPoignee } from '../../geometrie/tables.js'; import { deplacer, deplacerVue, dessinDepuisEcran, transformDe } from '../../geometrie/vue.js'; import { TABLE as LIBELLES } from '../libelles/plan.js'; +import { commandeDeDepot, reservationTenue } from './depot.js'; import { REPOS, cleFantome, geste, libellePlace, memoriser } from './gestes.js'; const SEUIL = constante('SEUIL_GLISSER_PX'); @@ -371,14 +372,11 @@ describe('les libellés d’un geste : le fantôme et la place d’une pose', () test('cleFantome : au-dessus du sol, une réservation « ce tour » se dit retirée, une « tous les tours » refusée (§ 5.8)', () => { // Le dépôt sur le sol retire la première et refuse la seconde : le // fantôme dit ce qui arrivera au relâcher, non ce qui arrive sur une - // chaise. Une personne que rien ne tient, et les autres cibles, gardent - // leur libellé. + // chaise. La vue seule garde son libellé. assert.equal(cleFantome('reservations', 'tour', true), 'plan.fantome.retireReservationTour'); assert.equal(cleFantome('reservations', 'tous', true), 'plan.fantome.refuseTous'); assert.equal(cleFantome('reservations', 'tour', false), 'plan.fantome.reservationTour'); assert.equal(cleFantome('reservations', 'tous', false), 'plan.fantome.reservationTous'); - assert.equal(cleFantome('reservations', null, true), 'plan.fantome.reservations'); - assert.equal(cleFantome('retenu', null, true), 'plan.fantome.retenu'); assert.equal(cleFantome(null, 'tous', true), 'plan.fantome.vueSeule'); assert.match(LIBELLES.get('plan.fantome.retireReservationTour'), /retire/); assert.match(LIBELLES.get('plan.fantome.refuseTous'), /refusé/); @@ -388,6 +386,70 @@ describe('les libellés d’un geste : le fantôme et la place d’une pose', () } }); + test('cleFantome : au-dessus du sol, le retenu se dit retiré du tour ; une personne que rien ne tient, rien ne change', () => { + // Sous 'retenu', le dépôt sur le sol est retirerDuTour : la personne + // attend dans la réserve (§ 7.1). Sous 'reservations', une personne + // qu'aucune réservation ne tient n'a rien à retirer : le dépôt ne rend + // rien, et le fantôme ne promet pas une réservation. + for (const portee of [null, 'tour', 'tous']) { + assert.equal(cleFantome('retenu', portee, true), 'plan.fantome.retireDuTour'); + assert.equal(cleFantome('retenu', portee, false), 'plan.fantome.retenu'); + } + assert.equal(cleFantome('reservations', null, true), 'plan.fantome.rienNeChange'); + assert.equal(cleFantome('reservations', null, false), 'plan.fantome.reservations'); + assert.match(LIBELLES.get('plan.fantome.retireDuTour'), /retire du tour/); + assert.match(LIBELLES.get('plan.fantome.retireDuTour'), /réserve/); + assert.match(LIBELLES.get('plan.fantome.rienNeChange'), /rien ne change/); + }); + + test('cleFantome dit l’effet du relâcher : chaque clé, sur chaque cible, portée et point, suit la commande de commandeDeDepot', () => { + // Le fantôme et le dépôt lisent la même réservation (reservationTenue), + // comme Plan.svelte : la personne 11, assise à la table 1, siège 1, sans + // réservation, ou tenue « ce tour » ou « tous les tours ». Elle se + // dépose sur une chaise libre, sur une chaise occupée, ou sur le sol. + // L'effet est la commande exécutée, le code du refus rendu, ou « rien ». + const EFFETS = new Map([ + ['plan.fantome.reservations', ['reserver']], + ['plan.fantome.reservationTour', ['deplacerReservation']], + ['plan.fantome.reservationTous', ['deplacerReservation']], + ['plan.fantome.retireReservationTour', ['retirerReservation']], + ['plan.fantome.refuseTous', ['RESERVATION_TOUS_LES_TOURS']], + ['plan.fantome.retenu', ['deplacerDansRetenu', 'echangerDansRetenu']], + ['plan.fantome.retireDuTour', ['retirerDuTour']], + ['plan.fantome.rienNeChange', ['rien']], + ['plan.fantome.vueSeule', ['VUE_SEULE']], + ]); + const RESERVATIONS = [null, { participant: 11, portee: 'tour', tour: 1 }, { participant: 11, portee: 'tous', tour: null }]; + const POINTS = [ + { cible: { table: 2, siege: 1 }, surLeSol: false }, + { cible: { table: 2, siege: 2 }, surLeSol: false }, + { cible: null, surLeSol: true }, + ]; + const depuis = { table: 1, siege: 1 }; + const vus = new Set(); + for (const cible of ['reservations', 'retenu', null]) { + for (const reservation of RESERVATIONS) { + const etat = { + cible, + refus: cible === null ? { code: 'VUE_SEULE', details: {} } : null, + tables: [ + { id: 1, chaises: [{ siege: 1, occupant: 11, reservation }, { siege: 2, occupant: 12, reservation: null }] }, + { id: 2, chaises: [{ siege: 1, occupant: null, reservation: null }, { siege: 2, occupant: 13, reservation: null }] }, + ], + }; + for (const point of POINTS) { + const cle = cleFantome(cible, reservationTenue(etat, 11, depuis)?.portee ?? null, point.surLeSol); + const resultat = commandeDeDepot({ etatPlaces: etat, tour: 1, personne: 11, depuis, ...point }); + const effet = resultat === null ? 'rien' : (resultat.refus?.code ?? resultat.commande); + const cas = JSON.stringify({ cible, reservation: reservation?.portee ?? null, point }); + assert.ok(EFFETS.get(cle).includes(effet), `${cas} : le fantôme dit ${cle}, le relâcher fait ${effet}`); + vus.add(cle); + } + } + } + assert.deepEqual([...vus].sort(), [...EFFETS.keys()].sort()); + }); + test('cleFantome : une cible hors du contrat lève, au lieu de se dire « vue seule »', () => { for (const cible of ['reservation', 'Retenu', undefined, '', 0]) { assert.throws(() => cleFantome(cible), { name: 'TypeError', message: /^cible du fantôme hors du contrat/ });