diff --git a/doc/captures/07a-cibles-sieges.png b/doc/captures/07a-cibles-sieges.png index 0664904..e2025f6 100644 Binary files a/doc/captures/07a-cibles-sieges.png and b/doc/captures/07a-cibles-sieges.png differ diff --git a/doc/captures/manifeste.json b/doc/captures/manifeste.json index 8263075..01eacab 100644 --- a/doc/captures/manifeste.json +++ b/doc/captures/manifeste.json @@ -8,7 +8,7 @@ {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"082a133e76c809940ead417be7cdf66077972ad229b85bac12e80f4b007f3307","etape":"4","etat":"421c6294b4348fa49aa0105ea643fd11c80806a9fbb1e079435adac079e6b260","facteurEchelle":1,"fichier":"05-tables-etat","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"6eb751e9553f8dcf77d07c918f43c5da865b15f2a7f516bb9eb8b87cd8a87a1f"},"instant":"2026-10-06T19:00: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","Tables et tours","Places par défaut","Nombre de tours","Tables","Ajouter une table ronde","Ajouter une table carrée","Table","1","Ronde","Suit le défaut","Places","Supprimer","Table","2","Ronde","Surchargée","Places","Réaligner","Supprimer"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"6a48fe8ea1922ed946072eefdd7ca53833d7fe4bd2420543e9ebd3c40612e179","etape":"4","etat":"421c6294b4348fa49aa0105ea643fd11c80806a9fbb1e079435adac079e6b260","facteurEchelle":1,"fichier":"06-defaut-consequence","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"742079465f67a44ae4bcc8028b79b4a1e9bab8c4656d9b633b4d8d6c8455f9a6"},"instant":"2026-10-06T19:00: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","Tables et tours","Places par défaut","1 table suit le défaut et passe de 8 à 10 places ; 1 table surchargée (8) ne change pas.","Total : 16 → 18 sièges pour 12 participants.","Appliquer","Appliquer et réaligner la surchargée","Garder le défaut actuel","Nombre de tours","Tables","Ajouter une table ronde","Ajouter une table carrée","Table","1","Ronde","Suit le défaut","Places","Supprimer","Table","2","Ronde","Surchargée","Places","Réaligner","Supprimer"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"e650f15bb5c51bec4160ff76219fe77902bf42d1e9ab47d46b9f81e4c5055844","etape":"4","etat":"7d42132a2caa999f9d046f580b1093732ebb44984b8fdec5b27da7e42801678e","facteurEchelle":1,"fichier":"07-plan-tables","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"ba04243df820caa656e7884ce0e9db02912b6d6203cd0efe889ff5dbe0052354"},"instant":"2026-10-06T19:00: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-chaise-trait","ratio":7.58,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":15.1,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":15.1,"role":"texte","seuil":4.5},{"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-trait","ratio":8.89,"role":"marque","seuil":3},{"jeton":"--couleur-echelle","ratio":14.62,"role":"marque","seuil":3},{"jeton":"--couleur-page-texte","ratio":14.62,"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","Plan","Réservations","Tour affiché","1","2","3","Zoom +","Zoom −","Ajuster","Listes de noms","Légende","Plein écran","1. (libre)","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","9. (libre)","10. (libre)","1. (libre)","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","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 12 mobiles","Listes de travail","Titres de place non pourvus","Aucun.","Titres pressentis sans réservation","Basile Lanterneau","— titre pressenti","animation","Gaspard Marbrelle","— titre pressenti","animation","Réserve","Personnes en attente au tour 1","Basile Lanterneau","Capucine Pommerel","Gaspard Marbrelle","Ysaline Sablonnet","Anselme Givrecœur","Côme Fenouillet","Apolline Coudraie","Basile Lanterneau","Brunœil","Faustine Ormelin","Edgar Ardoisier","Bertille Cressonnier"],"theme":"clair","versionReelle":"2026.10.05.01"}, -{"cadre":{"hauteur":700,"largeur":1366},"empreinte":"4fc279e4a15bec53d4d467639d0cf660bdee15cd02f5d875f6e26c6058240b99","etape":"4","etat":"4d37d30748068354162752743c26aa7e0ab00d6e9b2aa1996b6f9fb410848280","facteurEchelle":1,"fichier":"07a-cibles-sieges","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"9ab4237de86815cd8124695b59d7bf55e5bb87ba95361df45fd0df5393e428f0"},"instant":"2026-10-06T19:00: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-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-echelle","ratio":15.1,"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-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-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-echelle","ratio":14.62,"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-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","Lecture","Liste des événements","Clair","Sombre","À propos","Modifier","Lecture","Défaire","Grande démonstration 1","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Import","Participants","Tables et tours","Plan","Diagnostic","Génération","Propositions","Plan","Réservations","Tour affiché","1","2","3","4","Zoom +","Zoom −","Ajuster","Listes de noms","Légende","Plein écran","1. (réservée, tous les tours)","Manon Urvançon","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Guillaume Luskic…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Fatima Korvouze","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Antoine Bralmault","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Manon Tolveron","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Philippe Voskeval","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Félix Voskande","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Michel Korveron","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Gabriel Ivreval","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Marc Vandergrol…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Étienne Tolvançon","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Sophie Pruvotte","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Fatima Abrevanç…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Marie Urvard","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Catherine Hulver…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Camille Neskard","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Youssef Neskière","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Guillaume Dulvivet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Philippe Ivroubre","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Claire Jusvivet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Youssef Jusvard","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Louise Neskichon","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Hugo Bralmouze","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Gabriel Abrevault","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Louis Selvivet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Jean Fralvumet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Marie Bralmesson","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Thomas Selvignac","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Émilie Abrevière","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Geneviève Dulvu…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","1. (réservée, tous les tours)","François Obrouze","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","1. (réservée, tous les tours)","François Ivrivet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","1. (réservée, tous les tours)","Samuel Bralmeux","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","1","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","Diagnostic","écart d'itinéraire maximal : plancher 1, au moins 24 mobiles finissent sous leur plafond a priori","plafond a priori 24 rencontres pour 4 ancrés","plafond a priori 28 rencontres pour 29 ancrés et 227 mobiles","99 retours imposés par 33 ancrages","Listes de travail","Titres de place non pourvus","Aucun.","Titres pressentis sans réservation","Aucun.","Réserve","Personnes en attente au tour 1","Isabelle Mirvault","Félix Abrevande","Hugo Fralvançon","Manon Fralveux","Catherine Luskotte","Zoé WAMMERWOLM-MAWWEMMEL","Catherine Urvesson","Karim Hulvivet","Caroline Selveron","Catherine Korvault","Caroline Fralvotte","Geneviève Jusvesson","Paul Abrevumet","Léa Fralvault","Fatima Obrivet","Zoé Dulveval","Paul Korveux","Julie Luskière","Émilie Clovande","Paul Hulvande","Hugo Fralvesson","Isabelle Urvière","Thomas Rozvotte","Isabelle Bralmumet","Catherine Dulvouze","Marc Voskichon","Simon Mirvivet","Louise Clovault","Lucie Voskeux","Amina Eskignac","Luc Selvoubre","Étienne Fralvichon","Philippe Voskivet","Louise Quilmançon","Sophie Luskande","Pierre Abreveron","Philippe Neskeux","Amina Neskançon","Léa Tolvande","Antoine Quilmeron","Guillaume Clovumet","Michel Eskeux","Louise Hulvault","Samuel Eskumet","Claire Fralvivet","Léa Mirveux","Karim Gorzesson","Sylvie Quilmande","François Gorzichon","Amina Pruvesson","Lucie Quilmière","Fatima Korvesson","Nicolas Selvesson","Louise Neskesson","Lucie Tolvesson","Olivier Quilmesson","Guillaume Luskivet","Gabriel Neskault","Antoine Urvumet","Olivier Jusvançon","Simon Dulvançon","Chloé Gorzard","Gabriel Pruveux","Manon Clovière","Karim Tolveval","Hugo Bérêçàuvïlle-Dœûmôntëÿ","Claire Gorzeron","Hélène Luskumet","Louis Fralvande","Émilie Selvault","Léa Neskumet","Isabelle Korvichon","Michel Obrande","Isabelle Luskeval","Fatima Tolvoubre","Zoé Gorzière","Sylvie Korvignac","Antoine Obreval","Camille Tolvumet","Marie Dulvesson","Guillaume Tolvignac","Gabriel Mirvotte","Paul Neskignac","Raphaël Ivrotte","Marc Luskouze","Sylvie Jusvotte","Véronique Bralmoubre","Pierre Selvard","Marie Hulveux","Karim Fralveval","Philippe Gorzignac","Étienne Selvouze","Louis Hulvichon","Sylvie Urvoubre","Pierre Pruveval","Guillaume Hulvotte","Geneviève Abrevignac","Amina Bralmignac","Olivier Bralmançon","Marc Luskoubre","Geneviève Abrevoubre","Raphaël Korvoubre","Pierre Mirvard","Camille Jusvumet","Samuel Cloveval","Antoine Selvière","Véronique Jusvouze","Paul Neskivet","Samuel Dulvande","Lucie Selveval","Olivier Tolvichon","Fatima Hulvard","Alexandre Ivreux","Félix Korveval","Karim Urvichon","Émilie Dulvichon","Luc Tolvouze","Amina Quilmault","François Ivrouze","Hélène Gorzumet","Sophie Ivrault","Louise Bralmière","Nicolas Korvande","Geneviève Ivrumet","Geneviève Obresson","Guillaume Obrançon","Fatima Urvignac","Claire Bralmotte","Félix Hulvignac","François Clovotte","Philippe Dulvoubre","Samuel Rozveron","Pierre Hulvoubre","Émilie Urvouze","Félix Korvançon","Émilie Neskande","Chantal Mirvumet","Alexandre Korvotte","Zoé Eskeval","Nathalie Voskoubre","Sophie Pruvière","Paul Obrichon","Nathalie Eskière","Caroline Ivreron","Véronique Neskoubre","Anne Urvivet","Olivier Hulvançon","Karim Clovançon","Zoé Quilmotte","Félix Eskouze","Amina Rozvichon","Simon Fralvière","Pierre Hulvière","Raphaël Jusvault","Pierre Fralvoubre","Hélène Selveux","Caroline Rozvançon","Olivier Korvière","Nicolas Ivrichon","Félix Korvumet","Guillaume Selvumet","Michel Urvotte","Émilie Eskault","Hélène Bralmeval","Guillaume Hulvesson","Caroline Jusvoubre","Félix Luskeux","Antoine Pruvouze","Julie Quilmignac","Hugo Voskumet","Manon Quilmumet","Marc Voskouze","Samuel Abrevard","Manon Clovesson","Simon Hulvumet","Lucie Voskeron","Jean Neskeron","Manon Pruvivet","Sophie Clovard","Étienne Gorzeval","Gabriel Luskault","Émilie Korvard","Thomas Quilmichon","Alexandre Eskançon","Émilie Luskançon","Marc Dulvault","Hélène Rozveval","Julie Pruvançon","Louis Mirveron","François Eskard","Louis Ivrançon","Karim Clovivet","Léa Rozvande","Amina Hulvouze","Étienne Eskesson","Philippe Abreveval","Émilie Quilmeux","Simon Voskotte","Chantal Obrignac","Guillaume Eskotte","Léa Obrard","Guillaume Gorzivet","Geneviève Mirvançon","Julie Selvotte","Amina Mirvesson","Isabelle Luskignac","Chloé Obrumet","Anne Abrevesson","Fatima Rozvumet","Guillaume Mirvoubre","Véronique Rozvoubre","Claire Gorzotte","Antoine Ivrière","Isabelle Selvichon","Isabelle Urvault","Julie Voskesson","Chantal Abrevotte","Amina Neskotte","Étienne Voskançon","Zoé Fralveron","Marc Rozvouze","Sylvie Abreveux","Karim Pruvignac","Zoé Mirveval","Philippe Fralvignac","Gabriel Gorzeux","Jean Urveval"],"theme":"clair","versionReelle":"2026.10.05.01"}, +{"cadre":{"hauteur":700,"largeur":1366},"empreinte":"6aa6e1f767f7608b2d6382064b2f3c6e8b22a59ee1b2155ec262d1c47999bf2e","etape":"4","etat":"4d37d30748068354162752743c26aa7e0ab00d6e9b2aa1996b6f9fb410848280","facteurEchelle":1,"fichier":"07a-cibles-sieges","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"964fd3e038e1c2e09181edca01bbb597a3643b4e5176ca44b9ab8e38808f9c08"},"instant":"2026-10-06T19:00: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-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-echelle","ratio":15.1,"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-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-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-echelle","ratio":14.62,"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-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","Lecture","Liste des événements","Clair","Sombre","À propos","Modifier","Lecture","Défaire","Grande démonstration 1","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Import","Participants","Tables et tours","Plan","Diagnostic","Génération","Propositions","Plan","Réservations","Tour affiché","1","2","3","4","Zoom +","Zoom −","Ajuster","Listes de noms","Légende","Plein écran","1. (réservée, tous les tours)","Manon Urvançon","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Guillaume Luski…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Fatima Korvouze","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Antoine Bralmault","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Manon Tolveron","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Philippe Voskeval","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Félix Voskande","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Michel Korveron","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Gabriel Ivreval","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Marc Vandergro…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Étienne Tolvançon","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Sophie Pruvotte","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Fatima Abrevan…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Marie Urvard","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Catherine Hulver…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Camille Neskard","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Youssef Neskière","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Guillaume Dulvi…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Philippe Ivroubre","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Claire Jusvivet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Youssef Jusvard","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Louise Neskichon","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Hugo Bralmouze","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Gabriel Abrevault","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Louis Selvivet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Jean Fralvumet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Marie Bralmesson","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Thomas Selvignac","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Émilie Abrevière","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","1. (réservée, tous les tours)","Geneviève Dulv…","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","1. (réservée, tous les tours)","François Obrouze","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","1. (réservée, tous les tours)","François Ivrivet","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","1. (réservée, tous les tours)","Samuel Bralmeux","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","1","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","Diagnostic","écart d'itinéraire maximal : plancher 1, au moins 24 mobiles finissent sous leur plafond a priori","plafond a priori 24 rencontres pour 4 ancrés","plafond a priori 28 rencontres pour 29 ancrés et 227 mobiles","99 retours imposés par 33 ancrages","Listes de travail","Titres de place non pourvus","Aucun.","Titres pressentis sans réservation","Aucun.","Réserve","Personnes en attente au tour 1","Isabelle Mirvault","Félix Abrevande","Hugo Fralvançon","Manon Fralveux","Catherine Luskotte","Zoé WAMMERWOLM-MAWWEMMEL","Catherine Urvesson","Karim Hulvivet","Caroline Selveron","Catherine Korvault","Caroline Fralvotte","Geneviève Jusvesson","Paul Abrevumet","Léa Fralvault","Fatima Obrivet","Zoé Dulveval","Paul Korveux","Julie Luskière","Émilie Clovande","Paul Hulvande","Hugo Fralvesson","Isabelle Urvière","Thomas Rozvotte","Isabelle Bralmumet","Catherine Dulvouze","Marc Voskichon","Simon Mirvivet","Louise Clovault","Lucie Voskeux","Amina Eskignac","Luc Selvoubre","Étienne Fralvichon","Philippe Voskivet","Louise Quilmançon","Sophie Luskande","Pierre Abreveron","Philippe Neskeux","Amina Neskançon","Léa Tolvande","Antoine Quilmeron","Guillaume Clovumet","Michel Eskeux","Louise Hulvault","Samuel Eskumet","Claire Fralvivet","Léa Mirveux","Karim Gorzesson","Sylvie Quilmande","François Gorzichon","Amina Pruvesson","Lucie Quilmière","Fatima Korvesson","Nicolas Selvesson","Louise Neskesson","Lucie Tolvesson","Olivier Quilmesson","Guillaume Luskivet","Gabriel Neskault","Antoine Urvumet","Olivier Jusvançon","Simon Dulvançon","Chloé Gorzard","Gabriel Pruveux","Manon Clovière","Karim Tolveval","Hugo Bérêçàuvïlle-Dœûmôntëÿ","Claire Gorzeron","Hélène Luskumet","Louis Fralvande","Émilie Selvault","Léa Neskumet","Isabelle Korvichon","Michel Obrande","Isabelle Luskeval","Fatima Tolvoubre","Zoé Gorzière","Sylvie Korvignac","Antoine Obreval","Camille Tolvumet","Marie Dulvesson","Guillaume Tolvignac","Gabriel Mirvotte","Paul Neskignac","Raphaël Ivrotte","Marc Luskouze","Sylvie Jusvotte","Véronique Bralmoubre","Pierre Selvard","Marie Hulveux","Karim Fralveval","Philippe Gorzignac","Étienne Selvouze","Louis Hulvichon","Sylvie Urvoubre","Pierre Pruveval","Guillaume Hulvotte","Geneviève Abrevignac","Amina Bralmignac","Olivier Bralmançon","Marc Luskoubre","Geneviève Abrevoubre","Raphaël Korvoubre","Pierre Mirvard","Camille Jusvumet","Samuel Cloveval","Antoine Selvière","Véronique Jusvouze","Paul Neskivet","Samuel Dulvande","Lucie Selveval","Olivier Tolvichon","Fatima Hulvard","Alexandre Ivreux","Félix Korveval","Karim Urvichon","Émilie Dulvichon","Luc Tolvouze","Amina Quilmault","François Ivrouze","Hélène Gorzumet","Sophie Ivrault","Louise Bralmière","Nicolas Korvande","Geneviève Ivrumet","Geneviève Obresson","Guillaume Obrançon","Fatima Urvignac","Claire Bralmotte","Félix Hulvignac","François Clovotte","Philippe Dulvoubre","Samuel Rozveron","Pierre Hulvoubre","Émilie Urvouze","Félix Korvançon","Émilie Neskande","Chantal Mirvumet","Alexandre Korvotte","Zoé Eskeval","Nathalie Voskoubre","Sophie Pruvière","Paul Obrichon","Nathalie Eskière","Caroline Ivreron","Véronique Neskoubre","Anne Urvivet","Olivier Hulvançon","Karim Clovançon","Zoé Quilmotte","Félix Eskouze","Amina Rozvichon","Simon Fralvière","Pierre Hulvière","Raphaël Jusvault","Pierre Fralvoubre","Hélène Selveux","Caroline Rozvançon","Olivier Korvière","Nicolas Ivrichon","Félix Korvumet","Guillaume Selvumet","Michel Urvotte","Émilie Eskault","Hélène Bralmeval","Guillaume Hulvesson","Caroline Jusvoubre","Félix Luskeux","Antoine Pruvouze","Julie Quilmignac","Hugo Voskumet","Manon Quilmumet","Marc Voskouze","Samuel Abrevard","Manon Clovesson","Simon Hulvumet","Lucie Voskeron","Jean Neskeron","Manon Pruvivet","Sophie Clovard","Étienne Gorzeval","Gabriel Luskault","Émilie Korvard","Thomas Quilmichon","Alexandre Eskançon","Émilie Luskançon","Marc Dulvault","Hélène Rozveval","Julie Pruvançon","Louis Mirveron","François Eskard","Louis Ivrançon","Karim Clovivet","Léa Rozvande","Amina Hulvouze","Étienne Eskesson","Philippe Abreveval","Émilie Quilmeux","Simon Voskotte","Chantal Obrignac","Guillaume Eskotte","Léa Obrard","Guillaume Gorzivet","Geneviève Mirvançon","Julie Selvotte","Amina Mirvesson","Isabelle Luskignac","Chloé Obrumet","Anne Abrevesson","Fatima Rozvumet","Guillaume Mirvoubre","Véronique Rozvoubre","Claire Gorzotte","Antoine Ivrière","Isabelle Selvichon","Isabelle Urvault","Julie Voskesson","Chantal Abrevotte","Amina Neskotte","Étienne Voskançon","Zoé Fralveron","Marc Rozvouze","Sylvie Abreveux","Karim Pruvignac","Zoé Mirveval","Philippe Fralvignac","Gabriel Gorzeux","Jean Urveval"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"5e94a05db66d052a94435895fb62675b9438247e0ce319cfdb38a743cdb3e547","etape":"4","etat":"4d37d30748068354162752743c26aa7e0ab00d6e9b2aa1996b6f9fb410848280","facteurEchelle":1,"fichier":"07a-cibles-tables","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"7ee04170e649d97b27eb4136d30cfb240a5e734a1b4f22eceabc6dc9f17e1961"},"instant":"2026-10-06T19:00: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-lecture","ratio":5.29,"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-page-texte","ratio":15.1,"role":"texte","seuil":4.5},{"jeton":"--couleur-poignee","ratio":7.58,"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-cadre-lecture","ratio":7.34,"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-page-texte","ratio":14.62,"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","Lecture","Liste des événements","Clair","Sombre","À propos","Modifier","Lecture","Défaire","Grande démonstration 1","Brouillon","Proposé","Retenu","Bloqué","Imprimer…","Import","Participants","Tables et tours","Plan","Diagnostic","Génération","Propositions","Plan","Réservations","Tour affiché","1","2","3","4","Zoom +","Zoom −","Ajuster","Listes de noms","Légende","Plein écran","5","m","disposition relative — l'encombrement dans la salle n'est pas vérifié","Diagnostic","écart d'itinéraire maximal : plancher 1, au moins 24 mobiles finissent sous leur plafond a priori","plafond a priori 24 rencontres pour 4 ancrés","plafond a priori 28 rencontres pour 29 ancrés et 227 mobiles","99 retours imposés par 33 ancrages","Listes de travail","Titres de place non pourvus","Aucun.","Titres pressentis sans réservation","Aucun.","Réserve","Personnes en attente au tour 1","Isabelle Mirvault","Félix Abrevande","Hugo Fralvançon","Manon Fralveux","Catherine Luskotte","Zoé WAMMERWOLM-MAWWEMMEL","Catherine Urvesson","Karim Hulvivet","Caroline Selveron","Catherine Korvault","Caroline Fralvotte","Geneviève Jusvesson","Paul Abrevumet","Léa Fralvault","Fatima Obrivet","Zoé Dulveval","Paul Korveux","Julie Luskière","Émilie Clovande","Paul Hulvande","Hugo Fralvesson","Isabelle Urvière","Thomas Rozvotte","Isabelle Bralmumet","Catherine Dulvouze","Marc Voskichon","Simon Mirvivet","Louise Clovault","Lucie Voskeux","Amina Eskignac","Luc Selvoubre","Étienne Fralvichon","Philippe Voskivet","Louise Quilmançon","Sophie Luskande","Pierre Abreveron","Philippe Neskeux","Amina Neskançon","Léa Tolvande","Antoine Quilmeron","Guillaume Clovumet","Michel Eskeux","Louise Hulvault","Samuel Eskumet","Claire Fralvivet","Léa Mirveux","Karim Gorzesson","Sylvie Quilmande","François Gorzichon","Amina Pruvesson","Lucie Quilmière","Fatima Korvesson","Nicolas Selvesson","Louise Neskesson","Lucie Tolvesson","Olivier Quilmesson","Guillaume Luskivet","Gabriel Neskault","Antoine Urvumet","Olivier Jusvançon","Simon Dulvançon","Chloé Gorzard","Gabriel Pruveux","Manon Clovière","Karim Tolveval","Hugo Bérêçàuvïlle-Dœûmôntëÿ","Claire Gorzeron","Hélène Luskumet","Louis Fralvande","Émilie Selvault","Léa Neskumet","Isabelle Korvichon","Michel Obrande","Isabelle Luskeval","Fatima Tolvoubre","Zoé Gorzière","Sylvie Korvignac","Antoine Obreval","Camille Tolvumet","Marie Dulvesson","Guillaume Tolvignac","Gabriel Mirvotte","Paul Neskignac","Raphaël Ivrotte","Marc Luskouze","Sylvie Jusvotte","Véronique Bralmoubre","Pierre Selvard","Marie Hulveux","Karim Fralveval","Philippe Gorzignac","Étienne Selvouze","Louis Hulvichon","Sylvie Urvoubre","Pierre Pruveval","Guillaume Hulvotte","Geneviève Abrevignac","Amina Bralmignac","Olivier Bralmançon","Marc Luskoubre","Geneviève Abrevoubre","Raphaël Korvoubre","Pierre Mirvard","Camille Jusvumet","Samuel Cloveval","Antoine Selvière","Véronique Jusvouze","Paul Neskivet","Samuel Dulvande","Lucie Selveval","Olivier Tolvichon","Fatima Hulvard","Alexandre Ivreux","Félix Korveval","Karim Urvichon","Émilie Dulvichon","Luc Tolvouze","Amina Quilmault","François Ivrouze","Hélène Gorzumet","Sophie Ivrault","Louise Bralmière","Nicolas Korvande","Geneviève Ivrumet","Geneviève Obresson","Guillaume Obrançon","Fatima Urvignac","Claire Bralmotte","Félix Hulvignac","François Clovotte","Philippe Dulvoubre","Samuel Rozveron","Pierre Hulvoubre","Émilie Urvouze","Félix Korvançon","Émilie Neskande","Chantal Mirvumet","Alexandre Korvotte","Zoé Eskeval","Nathalie Voskoubre","Sophie Pruvière","Paul Obrichon","Nathalie Eskière","Caroline Ivreron","Véronique Neskoubre","Anne Urvivet","Olivier Hulvançon","Karim Clovançon","Zoé Quilmotte","Félix Eskouze","Amina Rozvichon","Simon Fralvière","Pierre Hulvière","Raphaël Jusvault","Pierre Fralvoubre","Hélène Selveux","Caroline Rozvançon","Olivier Korvière","Nicolas Ivrichon","Félix Korvumet","Guillaume Selvumet","Michel Urvotte","Émilie Eskault","Hélène Bralmeval","Guillaume Hulvesson","Caroline Jusvoubre","Félix Luskeux","Antoine Pruvouze","Julie Quilmignac","Hugo Voskumet","Manon Quilmumet","Marc Voskouze","Samuel Abrevard","Manon Clovesson","Simon Hulvumet","Lucie Voskeron","Jean Neskeron","Manon Pruvivet","Sophie Clovard","Étienne Gorzeval","Gabriel Luskault","Émilie Korvard","Thomas Quilmichon","Alexandre Eskançon","Émilie Luskançon","Marc Dulvault","Hélène Rozveval","Julie Pruvançon","Louis Mirveron","François Eskard","Louis Ivrançon","Karim Clovivet","Léa Rozvande","Amina Hulvouze","Étienne Eskesson","Philippe Abreveval","Émilie Quilmeux","Simon Voskotte","Chantal Obrignac","Guillaume Eskotte","Léa Obrard","Guillaume Gorzivet","Geneviève Mirvançon","Julie Selvotte","Amina Mirvesson","Isabelle Luskignac","Chloé Obrumet","Anne Abrevesson","Fatima Rozvumet","Guillaume Mirvoubre","Véronique Rozvoubre","Claire Gorzotte","Antoine Ivrière","Isabelle Selvichon","Isabelle Urvault","Julie Voskesson","Chantal Abrevotte","Amina Neskotte","Étienne Voskançon","Zoé Fralveron","Marc Rozvouze","Sylvie Abreveux","Karim Pruvignac","Zoé Mirveval","Philippe Fralvignac","Gabriel Gorzeux","Jean Urveval"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"03fa9a9b0f1b629d6d43dd536ae6da66ddfb105dd8bd6a68f65807235644183d","etape":"5","etat":"7e19a3b61721017693ee5895e1b4a82e2f1656e35e71989ac4d29ea9cfc59fe8","facteurEchelle":1,"fichier":"07b-nombre-de-tours","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"d49f53ecabd801b0b4681d5c4ccbd277c71ebe2b15c46e6d77e08a741faf4a90"},"instant":"2026-10-06T19:00:00-04:00","mode":"chromium-headless-shell","pdf":null,"source":"navigateur","styles":{"clair":[{"jeton":"--couleur-page-texte","ratio":15.1,"role":"texte","seuil":4.5}],"sombre":[{"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","Tables et tours","Places par défaut","Nombre de tours","Passer de 3 à 2 tours ? Ce changement emporte :","Basile Lanterneau : réservation à la table 1 au tour 3, détruite","Annuler","Confirmer","Tables","Ajouter une table ronde","Ajouter une table carrée","Table","1","Ronde","Suit le défaut","Places","Supprimer","Table","2","Ronde","Surchargée","Places","Réaligner","Supprimer"],"theme":"clair","versionReelle":"2026.10.05.01"}, {"cadre":{"hauteur":700,"largeur":1366},"empreinte":"683bc10e0c646219a177996b828093c6033d6ae959c82e6528967f61151b8227","etape":"6","etat":"0ad50f28307185683e15e5660c2da2f3398d783d68988d637c519ef3ba0658a5","facteurEchelle":1,"fichier":"08-animateurs-designes","graine":null,"image":{"hauteur":700,"largeur":1366,"sha256":"86e95804c0abe8acbe9b34fa0a43642ad26b39e7a56a6e335603c167988a49c2"},"instant":"2026-10-06T19:00: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-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-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-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-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-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-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","É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","Plan","Réservations","Tour affiché","1","2","3","Zoom +","Zoom −","Ajuster","Listes de noms","Légende","Plein écran","1. (réservée, tous les tours)","Basile Lanterneau","—","animation","2. (libre)","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","9. (libre)","10. (libre)","1. (libre)","— titre","animation","non pourvu","2. (réservée, tour 1)","Capucine Pommerel","3. (libre)","4. (libre)","5. (libre)","6. (libre)","7. (libre)","8. (libre)","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","Gaspard Marbrelle","Ysaline Sablonnet","Anselme Givrecœur","Côme Fenouillet","Apolline Coudraie","Basile Lanterneau","Brunœil","Faustine Ormelin","Edgar Ardoisier","Bertille Cressonnier","Table 2, siège 1","1. (libre)","— titre","animation","non pourvu","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","Désigner","Retirer le titre"],"theme":"clair","versionReelle":"2026.10.05.01"}, diff --git a/src/interface/impression/PlanImprime.navigateur.test.js b/src/interface/impression/PlanImprime.navigateur.test.js new file mode 100644 index 0000000..166c81d --- /dev/null +++ b/src/interface/impression/PlanImprime.navigateur.test.js @@ -0,0 +1,402 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Le plan imprimé dans un vrai moteur de rendu (§ 7.4, § 7.7, § 11.7, +// § 13.5) : l'aperçu du plan, une feuille [data-feuille] par page de la +// découpe, chacune portant un de page dans sa zone de contenu, et hors +// d'elle le cartouche, l'échelle graphique et les mentions. Chaque mesure se +// lit sur la boîte rendue et les attributs, jamais sur une classe : le +// cadrage de chaque contre la découpe recalculée de la bande et de la +// hauteur de ligne mesurées comme l'aperçu les mesure, la troncature des +// listes contre celle du plan à l'écran, la barre d'échelle, les mentions, +// la bande qui compte le cartouche, l'absence d'identifiant, de poignée et +// de tabulation, et les marques qui se lisent en noir et blanc. +// +// 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 { TABLES_APPLICATION } from '../../application/libelles.js'; +import { creerTraduction } from '../../application/traduction.js'; +import { texteLivre } from '../../demo/livrees.js'; +import { constante } from '../../geometrie/constantes.js'; +import { echelleGraphique } from '../../geometrie/echelle.js'; +import { TAILLE_TEXTE_LISTE_CM } from '../../geometrie/texte.js'; +import { analyser } from '../../stockage/document.js'; +import { fixerCadre } from '../../../test/navigateur/entrees.js'; +import { + attendre, + chargePrincipale, + cliquer, + creerContexte, + fichierReglages, + ouvrirApercu, + ouvrirEvenement, +} from '../../../test/navigateur/ecrans.js'; +import { TABLES_INTERFACE } from '../libelles.js'; +import { CONFLITS } from '../marques.js'; +import { hauteurLigne } from '../plan/mesure_texte.js'; +import { libelleLongueur, modeleDessin } from '../plan/modele.js'; +import { nomDeCharge } from '../plan/vue_plan.js'; +import { MM_PAR_PX, bandeDeFeuille, feuilleDe, optionsApercu, sortieApercu } from './modele.js'; +import { LARGEUR_ECHELLE_MM, pagesDuPlan, renvois } from './plan.js'; + +const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]); + +// Images attendues au plus par étape. +const IMAGES = 300; + +// Une barre d'échelle se mesure au vingtième de millimètre (§ 7.7). +const TOLERANCE_BARRE_MM = 0.05; +const TOLERANCE_MM = 0.1; + +const SVG = 'http://www.w3.org/2000/svg'; + +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 dans = (m, selecteur) => m.racine.querySelector(selecteur); +const tous = (m, selecteur) => [...m.racine.querySelectorAll(selecteur)]; +const feuilles = (m) => tous(m, '[data-impression][data-vue-impression="plan"] [data-feuille]'); +const tuileDe = (feuille) => feuille.querySelector(':scope [data-contenu] > svg'); +const mm = (px) => px * MM_PAR_PX; +const plat = (chaine) => chaine.replace(/\s+/g, ' ').trim(); + +// Le viewBox d'un , lu de son attribut : les nombres tels que la vue +// les a écrits, sans l'arrondi de viewBox.baseVal. +function viewBoxDe(svg) { + const [x, y, largeur, hauteur] = svg.getAttribute('viewBox').trim().split(/\s+/).map(Number); + return { x, y, largeur, hauteur }; +} + +// Les lignes de chaque liste de noms sous racine, par table : le texte de +// chaque rangée, l'en-tête compris. +function listesSous(racine) { + return [...racine.querySelectorAll('text[data-liste]')].map((liste) => ({ + table: liste.dataset.liste, + lignes: [...liste.children].map((rangee) => rangee.textContent), + })); +} + +// La bande que l'aperçu a mesurée, relue comme il la mesure : sur la feuille +// gabarit, la seule feuille hors du document imprimé. +function bandeDuGabarit(m) { + const contenus = tous(m, '[data-ecran="apercu"] [data-contenu]').filter((contenu) => contenu.closest('[data-impression]') === null); + expect(contenus, 'la feuille gabarit').toHaveLength(1); + return bandeDeFeuille({ + feuillePx: contenus[0].parentElement.getBoundingClientRect().height, + contenuPx: contenus[0].getBoundingClientRect().height, + margeMm: constante('MARGE_PAGE_MM'), + }); +} + +// La hauteur de ligne des listes, mesurée comme la vue la mesure : une +// sonde au style des listes, sous le document imprimé. +function hauteurDesListes(m) { + const svg = document.createElementNS(SVG, 'svg'); + const texte = document.createElementNS(SVG, 'text'); + texte.setAttribute('font-size', String(TAILLE_TEXTE_LISTE_CM)); + texte.style.fontFamily = 'var(--police-interface)'; + svg.append(texte); + dans(m, '[data-impression]').append(svg); + try { + return hauteurLigne(texte); + } finally { + svg.remove(); + } +} + +// Les pages attendues du plan que l'aperçu montre, recalculées par +// pagesDuPlan sur la même sortie, la même bande et la même hauteur de ligne. +function pagesAttendues(m, charge) { + const options = optionsApercu(m.ui.etat().apercu, 'A4'); + const { etat, tour } = sortieApercu(charge, options, m.ui.etat().proposition); + const modele = modeleDessin(etat, { listes: options.listes, nomDe: nomDeCharge(charge), impression: true }); + const pied = feuilleDe({ nom: charge.evenement.nom, date: null, tour, horodatage: null, version: 'epreuve' }, false); + return pagesDuPlan( + etat, + modele.tables.map((table) => table.geometrie), + options.decoupe, + { papier: 'A4', bandeMm: bandeDuGabarit(m), listes: options.listes, hauteurLigne: hauteurDesListes(m), pied }, + ); +} + +// Change la découpe du plan par son sélecteur, et attend le document. +async function decouper(m, decoupe) { + const choix = dans(m, '[data-ecran="apercu"] [data-champ="decoupe"]'); + choix.value = decoupe; + choix.dispatchEvent(new Event('change', { bubbles: true })); + await attendre(`la découpe ${decoupe}`, () => m.ui.etat().apercu.decoupe === decoupe && feuilles(m).length > 0, IMAGES); +} + +// La principale, trois personnes d'une même table renommées des noms qui +// cassent une liste : de larges capitales, une ligature, deux cents +// caractères. +const NOMS_LONGS = ['W'.repeat(40), '\u{00C6}'.repeat(40), `Ondine ${'Lacasse'.repeat(28)}`.slice(0, 200)]; +function chargeAuxNomsLongs() { + const charge = chargePrincipale(); + for (const [rang, nom] of NOMS_LONGS.entries()) { + const personne = charge.participants[rang]; + personne.prenom = null; + personne.nom = nom; + } + return charge; +} + +describe('les pages du plan imprimé (§ 11.7)', () => { + test('un de page par feuille, autant que de pages ; le viewBox de chacun est le cadrage de sa page, à 1e-9 près', async () => { + for (const decoupe of ['auto', 'decoupe']) { + const charge = analyser(texteLivre('grande')).charge; + const m = await ouvrirApercu(contexte, charge, { section: 'participants' }); + await decouper(m, decoupe); + const attendues = pagesAttendues(m, charge); + const lues = feuilles(m); + expect(lues).toHaveLength(attendues.length); + expect(tous(m, '[data-impression] [data-contenu] > svg')).toHaveLength(attendues.length); + for (const [rang, feuille] of lues.entries()) { + const tuile = tuileDe(feuille); + expect(tuile, `la tuile de la feuille ${rang}`).not.toBeNull(); + const lu = viewBoxDe(tuile); + for (const cote of ['x', 'y', 'largeur', 'hauteur']) { + expect(Math.abs(lu[cote] - attendues[rang].viewBox[cote]), `${decoupe}, page ${rang}, ${cote}`).toBeLessThanOrEqual(1e-9); + } + expect(tuile.getAttribute('role')).toBe('img'); + expect(tuile.getAttribute('aria-label')).toBe(t('impression.plan.page', { page: rang + 1, total: attendues.length })); + expect(tuile.querySelector(':scope > g').getAttribute('aria-hidden')).toBe('true'); + } + await contexte.defaire(); + } + }); + + test('le cartouche rendu de chaque page, découpée : son repérage, puis un renvoi par page voisine, dans l’ordre haut, droite, bas, gauche, et rien d’autre', async () => { + const charge = analyser(texteLivre('grande')).charge; + const m = await ouvrirApercu(contexte, charge, { section: 'participants' }); + await decouper(m, 'decoupe'); + const attendues = pagesAttendues(m, charge); + const lues = feuilles(m); + expect(lues).toHaveLength(attendues.length); + // La découpe porte des pages voisines de deux côtés au moins : sans + // elles, un renvoi omis ou mal numéroté passerait inaperçu. + expect(Math.max(...attendues.map(({ cartouche }) => renvois(cartouche).length)), 'des voisines sur deux côtés au moins').toBeGreaterThanOrEqual(2); + for (const [rang, feuille] of lues.entries()) { + const cartouches = [...feuille.querySelectorAll('[data-cartouche]')]; + expect(cartouches, `le cartouche de la feuille ${rang}`).toHaveLength(1); + const { cartouche } = attendues[rang]; + const reperage = t('impression.plan.cartouche', cartouche); + const textes = renvois(cartouche).map(({ cle, page }) => t(cle, { page })); + expect(plat(cartouches[0].textContent), `feuille ${rang}`).toContain(reperage); + expect([...cartouches[0].children].map((partie) => plat(partie.textContent)), `feuille ${rang}`).toEqual([reperage, ...textes]); + } + }); + + test('grande démonstration, listes montrées, A4 : découpée par défaut, chaque table sur une page au moins', async () => { + const charge = analyser(texteLivre('grande')).charge; + const depart = performance.now(); + const m = await ouvrirApercu(contexte, charge, { section: 'participants' }); + await attendre('les listes de chaque page', () => tous(m, '[data-impression] [data-liste] tspan').length > 0, IMAGES); + const duree = performance.now() - depart; + console.info(`[IM3] ouverture de l'aperçu, grande démonstration, listes montrées, A4 : ${Math.round(duree)} ms`); + expect(m.ui.etat().apercu.listes).toBe(true); + expect(m.ui.etat().apercu.decoupe).toBe('auto'); + expect(feuilles(m).length).toBeGreaterThan(1); + const portees = new Set(tous(m, '[data-impression] [data-table]').map((table) => Number(table.dataset.table))); + expect([...portees].sort((a, b) => a - b)).toEqual(charge.tables.map(({ id }) => id).sort((a, b) => a - b)); + }); +}); + +describe('la troncature des listes, celle de l’écran (§ 7.4)', () => { + test('les noms « WWW… », « ÆÆÆ… » et de deux cents caractères s’abrègent sur le papier comme à l’écran, ligne pour ligne', async () => { + const m = await ouvrirEvenement(contexte, chargeAuxNomsLongs(), { section: 'plan', options: { fichiers: [fichierReglages('A4')] } }); + await attendre('les listes du plan', () => tous(m, '[data-plan] [data-liste] tspan').length > 0, IMAGES); + const ecran = new Map(listesSous(dans(m, '[data-plan]')).map(({ table, lignes }) => [table, lignes])); + const marque = t('plan.liste.abreviation'); + expect([...ecran.values()].flat().filter((ligne) => ligne.includes(marque)).length, 'des lignes abrégées').toBeGreaterThanOrEqual(NOMS_LONGS.length); + await cliquer(m, '[data-ecran="evenement"] [data-commande="apercu"]'); + await attendre('les listes imprimées', () => tous(m, '[data-impression] [data-liste] tspan').length > 0, IMAGES); + const imprimees = listesSous(dans(m, '[data-impression]')); + expect(new Set(imprimees.map(({ table }) => table))).toEqual(new Set(ecran.keys())); + for (const { table, lignes } of imprimees) { + expect(lignes, `table ${table}`).toEqual(ecran.get(table)); + } + }); +}); + +describe('l’échelle graphique et les mentions de chaque page (§ 7.7)', () => { + test('la barre de chaque page mesure sa longueur à l’échelle de la page, au vingtième de mm ; sa longueur affichée est celle de libelleLongueur', async () => { + for (const decoupe of ['auto', 'unePage', 'decoupe']) { + const m = await ouvrirApercu(contexte, analyser(texteLivre('grande')).charge, { section: 'participants' }); + await decouper(m, decoupe); + for (const feuille of feuilles(m)) { + const tuile = tuileDe(feuille); + const echelle = mm(tuile.getBoundingClientRect().width) / viewBoxDe(tuile).largeur; + const { longueurCm } = echelleGraphique(echelle, LARGEUR_ECHELLE_MM); + const barres = [...feuille.querySelectorAll('[data-marque="echelle"]')]; + expect(barres).toHaveLength(1); + const barre = barres[0]; + expect(barre.closest('[data-contenu]')).toBeNull(); + const largeur = mm(barre.getBoundingClientRect().width); + expect(Math.abs(largeur - longueurCm * echelle), `${decoupe} : ${largeur} mm pour ${longueurCm} cm`).toBeLessThanOrEqual(TOLERANCE_BARRE_MM); + const longueur = libelleLongueur(longueurCm); + const attendu = `${longueur.valeur.toFixed(longueur.decimales).replace('.', ',')} ${t(longueur.cle)}`; + expect(plat(barre.parentElement.textContent)).toBe(attendu); + } + await contexte.defaire(); + } + }); + + test('chaque feuille porte la mention « disposition relative » ; celle des listes, sur chaque page qui en porte, tant que les listes sont montrées', async () => { + const m = await ouvrirApercu(contexte, analyser(texteLivre('grande')).charge, { section: 'participants' }); + const relatives = tous(m, '[data-impression] [data-mention="relative"]'); + expect(relatives.length).toBeGreaterThan(0); + expect(relatives).toHaveLength(feuilles(m).length); + for (const feuille of feuilles(m)) { + const mentions = feuille.querySelectorAll('[data-mention="relative"]'); + expect(mentions).toHaveLength(1); + expect(plat(mentions[0].textContent)).toBe(t('plan.echelle.mention')); + const listes = feuille.querySelectorAll('[data-mention="listes"]'); + const portees = feuille.querySelectorAll('[data-table]').length > 0; + expect(listes.length, `feuille ${feuille.dataset.feuille}`).toBe(portees ? 1 : 0); + if (portees) expect(plat(listes[0].textContent)).toBe(t('impression.plan.listes')); + } + await cliquer(m, '[data-ecran="apercu"] [data-commande="listes"]'); + await attendre('les listes masquées', () => m.ui.etat().apercu.listes === false && feuilles(m).length > 0, IMAGES); + await attendre('les mentions des listes retirées', () => tous(m, '[data-impression] [data-mention="listes"]').length === 0, IMAGES); + expect(tous(m, '[data-impression] [data-liste]')).toHaveLength(0); + expect(tous(m, '[data-impression] [data-mention="relative"]')).toHaveLength(feuilles(m).length); + }); +}); + +describe('la bande du plan (HAUTEUR_BANDE_IMPRESSION, § 14.1)', () => { + // La bande d'une feuille rendue, en mm : la feuille, moins sa zone de + // contenu, moins une marge à chaque bord. + const bandeDe = (feuille) => + mm(feuille.getBoundingClientRect().height) - + mm(feuille.querySelector('[data-contenu]').getBoundingClientRect().height) - + 2 * constante('MARGE_PAGE_MM'); + + test('la bande compte cartouche, mentions et barre, hors de la zone de contenu : elle dépasse le pied, et tout ce qu’elle porte tient dans la feuille', async () => { + const m = await ouvrirApercu(contexte, analyser(texteLivre('grande')).charge, { section: 'participants' }); + const marge = constante('MARGE_PAGE_MM'); + for (const feuille of feuilles(m)) { + for (const selecteur of ['[data-cartouche]', '[data-mention="relative"]', '[data-marque="echelle"]']) { + const element = feuille.querySelector(selecteur); + expect(element, selecteur).not.toBeNull(); + expect(element.closest('[data-contenu]'), selecteur).toBeNull(); + } + const pied = feuille.querySelector('[data-pied]'); + const cartouche = feuille.querySelector('[data-cartouche]'); + const hors = mm(pied.getBoundingClientRect().height) + mm(cartouche.getBoundingClientRect().height); + expect(bandeDe(feuille), `feuille ${feuille.dataset.feuille}`).toBeGreaterThan(hors); + // Le pied, dernier de la bande, finit dans la feuille, au-dessus de + // sa marge. + const bas = feuille.getBoundingClientRect().bottom; + expect(mm(bas - pied.getBoundingClientRect().bottom)).toBeGreaterThanOrEqual(marge - TOLERANCE_MM); + } + }); + + test('un pied grandi de 20 mm grandit la bande d’autant ; la tuile de chaque page a la hauteur de sa zone de contenu, avant comme après : la découpe reçoit la bande mesurée', async () => { + const charge = analyser(texteLivre('grande')).charge; + const m = await ouvrirApercu(contexte, charge, { section: 'participants' }); + // Chaque feuille : la hauteur de sa tuile, celle de sa zone de contenu, + // et sa bande. La bande se mesure en portrait, sur la feuille gabarit : + // elle ne dépend pas de l'orientation que la découpe choisit. + const mesurer = () => + feuilles(m).map((feuille) => ({ + tuile: mm(tuileDe(feuille).getBoundingClientRect().height), + contenu: mm(feuille.querySelector('[data-contenu]').getBoundingClientRect().height), + bande: bandeDe(feuille), + })); + const avant = mesurer(); + const feuilleDeStyle = document.createElement('style'); + feuilleDeStyle.textContent = '[data-pied] { padding-bottom: 20mm !important; }'; + document.head.append(feuilleDeStyle); + try { + await cliquer(m, '[data-ecran="apercu"] [data-commande="fermerApercu"]'); + await cliquer(m, '[data-ecran="evenement"] [data-commande="apercu"]'); + await attendre('le plan', () => feuilles(m).length > 0, IMAGES); + const apres = mesurer(); + for (const [quand, pages] of [['avant', avant], ['après', apres]]) { + for (const [rang, page] of pages.entries()) { + expect(Math.abs(page.tuile - page.contenu), `${quand}, page ${rang} : ${JSON.stringify(page)}`).toBeLessThanOrEqual(TOLERANCE_MM); + } + } + for (const [rang, page] of apres.entries()) { + expect(Math.abs(page.bande - avant[0].bande - 20), `page ${rang} : ${page.bande} − ${avant[0].bande}`).toBeLessThanOrEqual(TOLERANCE_MM); + } + } finally { + feuilleDeStyle.remove(); + } + }); +}); + +describe('le papier ne reçoit aucun geste (§ 11.7, § 13.5)', () => { + test('aucun identifiant en double dans le document ; sous [data-impression], aucun id hors des motifs, ni tabindex, ni [data-poignee], ni [data-plan]', async () => { + const m = await ouvrirApercu(contexte, analyser(texteLivre('grande')).charge, { section: 'participants' }); + await attendre('les sièges imprimés', () => tous(m, '[data-impression] [data-siege]').length > 0, IMAGES); + expect(feuilles(m).length).toBeGreaterThan(1); + const ids = [...document.querySelectorAll('[id]')].map((e) => e.id); + expect(ids.filter((id, rang) => ids.indexOf(id) !== rang)).toEqual([]); + // Les motifs se référencent par leur identifiant (url(#…)) : chaque page + // a les siens, dans ses . + const horsMotifs = tous(m, '[data-impression] [id]').filter((element) => element.closest('defs') === null); + expect(horsMotifs.map((element) => element.id)).toEqual([]); + for (const selecteur of ['[tabindex]', '[data-poignee]', '[data-plan]', '[data-table][role]', '[data-siege][role]', '[aria-activedescendant]']) { + expect(tous(m, `[data-impression] ${selecteur}`), selecteur).toEqual([]); + } + }); + + test('régime des sièges : chaque chaise de chaque table imprimée, quel que soit le zoom de l’écran', async () => { + const charge = analyser(texteLivre('grande')).charge; + const m = await ouvrirApercu(contexte, charge, { section: 'participants' }); + await decouper(m, 'unePage'); + const sieges = new Set(tous(m, '[data-impression] [data-siege]').map((siege) => siege.dataset.siege)); + const attendus = charge.tables.flatMap(({ id }) => { + const places = tous(m, `[data-impression] [data-table="${id}"] [data-siege]`).length; + expect(places, `table ${id}`).toBeGreaterThan(0); + return Array.from({ length: places }, (_, rang) => `${id}:${rang + 1}`); + }); + expect(sieges).toEqual(new Set(attendus)); + }); + + test('les trames de conflit, les cadenas, l’anneau et la hachure se peignent : le noir et blanc se lit par la forme', async () => { + // La principale, sa proposition : des tables en conflit, une + // désignation (anneau, cadenas fermé), un titre non pourvu (hachure, + // cadenas ouvert). + const m = await ouvrirApercu(contexte, chargePrincipale()); + await attendre('les sièges imprimés', () => tous(m, '[data-impression] [data-siege]').length > 0, IMAGES); + const peintes = (marque) => + tous(m, `[data-impression] [data-marque="${marque}"]`).filter((forme) => { + const boite = forme.getBoundingClientRect(); + return boite.width > 0 && boite.height > 0 && getComputedStyle(forme).visibility === 'visible'; + }); + const trames = CONFLITS.flatMap(({ marque }) => peintes(marque)); + expect(trames.length, 'trames de conflit').toBeGreaterThan(0); + for (const trame of trames) expect(getComputedStyle(trame).fill).toMatch(/^url\(/); + for (const marque of ['cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'contourConflit', 'pastille']) { + expect(peintes(marque).length, marque).toBeGreaterThan(0); + } + }); +}); + +describe('la commande de la vue', () => { + test('le plan imprimé reprend le tour de l’écran, et le pied le nomme', async () => { + const charge = chargePrincipale(); + expect(charge.evenement.tours).toBeGreaterThan(1); + const m = await ouvrirApercu(contexte, charge); + const choix = dans(m, '[data-ecran="apercu"] [data-champ="tour"]'); + choix.value = '2'; + choix.dispatchEvent(new Event('change', { bubbles: true })); + await attendre('le tour 2', () => m.ui.etat().apercu.tour === 2 && feuilles(m).length > 0, IMAGES); + for (const feuille of feuilles(m)) { + expect(plat(feuille.querySelector('[data-pied]').textContent)).toContain(t('impression.pied.tour', { tour: 2 })); + } + }); +}); diff --git a/src/interface/impression/PlanImprime.svelte b/src/interface/impression/PlanImprime.svelte index 1bc4a8e..15e08b0 100644 --- a/src/interface/impression/PlanImprime.svelte +++ b/src/interface/impression/PlanImprime.svelte @@ -2,61 +2,177 @@ License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) --> -{#if decoupe === null} - +{#snippet bandeDe(cartouche, echelle, mentions)} + {@const longueur = libelleLongueur(echelle.longueurCm)} +
+

+ {t('impression.plan.cartouche', cartouche)} + {#each renvois(cartouche) as renvoi (renvoi.sens)} + {t(renvoi.cle, { page: renvoi.page })} + {/each} +

+ +

{t('plan.echelle.mention')}

+ {#if mentions.listes} +

{t('impression.plan.listes')}

+ {/if} +
+{/snippet} + +{#if bandeMm === null} + + {#snippet bande()} + {@render bandeDe(CARTOUCHE_GABARIT, ECHELLE_GABARIT, MENTIONS_GABARIT)} + {/snippet} + {:else} - {#each decoupe.pages as page (page.rang)} - - {#snippet contenu()} -

{t('impression.plan.page', { page: page.cartouche.page, total: page.cartouche.total })}

- - {/snippet} -
- {/each} + + {#if pages !== null} + {#each pages as page (page.rang)} + + {#snippet contenu()} + + + + {/snippet} + {#snippet bande()} + {@render bandeDe(page.cartouche, page.echelle, page.mentions)} + {/snippet} + + {/each} + {/if} {/if} diff --git a/src/interface/impression/plan.js b/src/interface/impression/plan.js new file mode 100644 index 0000000..ebb063e --- /dev/null +++ b/src/interface/impression/plan.js @@ -0,0 +1,162 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Le plan imprimé (§ 7.7, § 11.7), sans DOM : les pages du plan d'un tour, +// chacune la tuile que la découpe (decouperPlan) lui donne, sous le pied +// d'un seul tour, avec son cartouche de repérage, la mention « disposition +// relative », l'échelle graphique de son échelle et, quand les listes sont +// montrées et que la page porte une table, la mention qui dit les listes +// hors d'échelle. Les blocs de noms entrent dans la découpe à la largeur du +// registre, LARGEUR_BLOC_NOMS_CM, celle de l'écran, et à la hauteur que leur +// donne la hauteur de ligne mesurée au rendu (HAUTEUR_LIGNE). Le module ne +// rend que des nombres et des données ; PlanImprime les pose. +import { constante } from '../../geometrie/constantes.js'; +import { echelleGraphique } from '../../geometrie/echelle.js'; +import { decouperPlan } from '../../geometrie/pages.js'; +import { englobantImpression } from '../../geometrie/tables.js'; + +/** Largeur maximale de la barre de l'échelle graphique imprimée, en mm de + * papier. */ +export const LARGEUR_ECHELLE_MM = 40; + +/** Le cartouche de la feuille gabarit : chaque renvoi présent, des numéros + * et une lettre de colonne plus larges que ceux d'un plan réel. La bande + * mesurée sur lui majore celle de toute page du plan. */ +export const CARTOUCHE_GABARIT = Object.freeze({ + page: 888, + total: 888, + colonne: 'WW', + rangee: 888, + voisins: Object.freeze({ haut: 887, droite: 887, bas: 887, gauche: 887 }), +}); + +/** Les mentions de la feuille gabarit : toutes deux présentes. */ +export const MENTIONS_GABARIT = Object.freeze({ relative: true, listes: true }); + +/** L'échelle graphique de la feuille gabarit : une barre de largeur + * LARGEUR_ECHELLE_MM ; sa hauteur ne dépend pas de sa longueur. */ +export const ECHELLE_GABARIT = Object.freeze({ longueurCm: 50000, largeurMm: LARGEUR_ECHELLE_MM }); + +// Les renvois d'un cartouche, dans l'ordre de la lecture du plan : le sens, +// et la clé de son libellé. +const RENVOIS = Object.freeze([ + ['haut', 'impression.plan.renvoi.haut'], + ['droite', 'impression.plan.renvoi.droite'], + ['bas', 'impression.plan.renvoi.bas'], + ['gauche', 'impression.plan.renvoi.gauche'], +]); + +/** + * La hauteur du bloc de noms de chaque table, en cm de dessin : une ligne + * par siège, de 1 à la capacité, plus l'en-tête d'une table gelée, plus une + * ligne. La ligne de base de la rangée i, comptée de 1, tombe i hauteurs de + * ligne sous le haut du bloc (ordonneesLignes, plan/modele.js) ; la ligne + * de plus majore le jambage de la dernière, plus court que la hauteur + * d'encre du gabarit. + * + * @param {{tables: Array<{id: number, capacite: number, gelee: boolean}>}} etat EtatPlaces + * @param {number} hauteurLigne cm, HAUTEUR_LIGNE mesurée au rendu + * @returns {Map} + * @throws {RangeError} une hauteur de ligne non finie ou ≤ 0 + */ +export function hauteursListes(etat, hauteurLigne) { + if (typeof hauteurLigne !== 'number' || !Number.isFinite(hauteurLigne) || hauteurLigne <= 0) { + throw new RangeError(`hauteur de ligne : nombre fini > 0 attendu, reçu ${String(hauteurLigne)}`); + } + return new Map(etat.tables.map(({ id, capacite, gelee }) => [id, (capacite + (gelee ? 1 : 0) + 1) * hauteurLigne])); +} + +// Lève RangeError quand le pied n'est pas celui d'un seul tour : le plan +// s'imprime tour par tour, et son pied le nomme. +function exigerPiedDUnTour(pied) { + const tour = pied?.elements?.find(({ element }) => element === 'tour'); + if (tour?.libelle?.cle !== 'impression.pied.tour') { + throw new RangeError(`pied du plan : le tour imprimé attendu, reçu ${JSON.stringify(tour?.libelle ?? null)}`); + } +} + +/** + * Les pages du plan imprimé d'un tour (§ 11.7). + * + * La découpe est celle de decouperPlan, sur l'englobant d'impression des + * géométries (englobantImpression) ; les listes montrées l'étendent de + * leurs blocs, larges de LARGEUR_BLOC_NOMS_CM, hauts de hauteursListes. Une + * page par tuile, dans l'ordre de la découpe : + * - rang, cartouche, tables : ceux de la tuile ; + * - orientation : celle de la découpe ; + * - viewBox : le cadrage de la tuile, en cm de dessin ; + * - taille : la tuile sur le papier, en mm, le cadrage à l'échelle ; + * - mentions : relative, toujours ; listes, quand les listes sont montrées + * et que la page porte une table ; + * - echelle : la longueur ronde dont la barre tient dans largeurEchelleMm + * à l'échelle de la découpe (echelleGraphique), et sa largeur en mm ; + * - pied : le pied reçu, le même sur chaque page. + * + * @param {Object} etat EtatPlaces du tour imprimé + * @param {import('../../geometrie/tables.js').GeometrieTable[]} geometries celles des tables de etat + * @param {'auto'|'unePage'|'decoupe'} decoupe + * @param {{papier: string, bandeMm: number, listes?: boolean, hauteurLigne?: number|null, + * pied: {elements: Object[], filigrane: boolean}, largeurEchelleMm?: number}} options + * pied : la feuille de feuilleDe (modele.js), d'un seul tour + * @returns {Array<{rang: number, orientation: 'portrait'|'paysage', + * viewBox: {x: number, y: number, largeur: number, hauteur: number}, + * taille: {largeur: number, hauteur: number}, tables: number[], + * mentions: {relative: true, listes: boolean}, echelle: {longueurCm: number, largeurMm: number}, + * cartouche: Object, pied: Object}>} + * @throws {RangeError} un pied de la soirée ; des listes montrées sans hauteur de ligne ; ce que + * decouperPlan refuse + */ +export function pagesDuPlan( + etat, + geometries, + decoupe, + { papier, bandeMm, listes = false, hauteurLigne = null, pied, largeurEchelleMm = LARGEUR_ECHELLE_MM }, +) { + exigerPiedDUnTour(pied); + const blocs = listes ? { largeur: constante('LARGEUR_BLOC_NOMS_CM'), hauteurs: hauteursListes(etat, hauteurLigne) } : null; + const resultat = decouperPlan(englobantImpression(geometries, { listes: blocs }), { + papier, + bandeMm, + mode: decoupe, + geometries, + listes: blocs, + }); + const { longueurCm, largeurPx } = echelleGraphique(resultat.echelle, largeurEchelleMm); + return resultat.pages.map(({ rang, cadrage, cartouche, tables }) => ({ + rang, + orientation: resultat.orientation, + viewBox: { ...cadrage }, + taille: { largeur: cadrage.largeur * resultat.echelle, hauteur: cadrage.hauteur * resultat.echelle }, + tables, + mentions: { relative: true, listes: listes && tables.length > 0 }, + echelle: { longueurCm, largeurMm: largeurPx }, + cartouche, + pied, + })); +} + +/** + * Les renvois d'un cartouche vers les pages voisines, dans l'ordre haut, + * droite, bas, gauche : le sens, la clé de son libellé et le numéro de la + * page, de 1 ; un bord du plan, sans voisine, n'en a pas. + * + * @param {{voisins: {haut: number|null, droite: number|null, bas: number|null, gauche: number|null}}} cartouche + * voisins : le rang de chaque page voisine, de 0 + * @returns {Array<{sens: string, cle: string, page: number}>} + */ +export function renvois({ voisins }) { + return RENVOIS.filter(([sens]) => voisins[sens] !== null).map(([sens, cle]) => ({ sens, cle, page: voisins[sens] + 1 })); +} + +/** + * Le modèle de dessin d'une page : les tables du modèle que la page porte, + * dans l'ordre du modèle — celui de la peinture —, le reste du modèle tel + * quel. Une table hors de la tuile ne se dessine pas sur la page. + * + * @param {{tables: Array<{id: number}>}} modele ModeleDessin + * @param {{tables: number[]}} page + */ +export function modeleDeLaPage(modele, { tables }) { + const portees = new Set(tables); + return { ...modele, tables: modele.tables.filter(({ id }) => portees.has(id)) }; +} diff --git a/src/interface/impression/plan.test.js b/src/interface/impression/plan.test.js new file mode 100644 index 0000000..d08b368 --- /dev/null +++ b/src/interface/impression/plan.test.js @@ -0,0 +1,217 @@ +// © 2026 TechnoLibre (http://www.technolibre.ca) +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +// Épreuves du plan imprimé, sans DOM (§ 7.7, § 11.7) : les pages que rend +// pagesDuPlan sont celles de la découpe (decouperPlan), une par feuille, +// chacune sous le pied d'un seul tour, la mention « disposition relative » +// et l'échelle graphique de son échelle ; la mention des listes sur chaque +// page qui en porte, quand elles sont montrées. La découpe attendue se +// recalcule ici par decouperPlan et englobantImpression, appelées +// directement ; la hauteur des listes, de la hauteur de ligne reçue. +import assert from 'node:assert/strict'; +import { describe, test } from '../../../test/lanceur.js'; +import { TABLES_APPLICATION } from '../../application/libelles.js'; +import { etatPlaces } from '../../application/places.js'; +import { creerTraduction } from '../../application/traduction.js'; +import { texteLivre } from '../../demo/livrees.js'; +import { echelleGraphique } from '../../geometrie/echelle.js'; +import { decouperPlan } from '../../geometrie/pages.js'; +import { HAUTEUR_LIGNE_SUPPOSEE, englobantImpression, geometrieTable } from '../../geometrie/tables.js'; +import { analyser, creerCharge } from '../../stockage/document.js'; +import { constante } from '../../geometrie/constantes.js'; +import { TABLES_INTERFACE } from '../libelles.js'; +import { modeleDessin } from '../plan/modele.js'; +import { feuilleDe } from './modele.js'; +import { + CARTOUCHE_GABARIT, + ECHELLE_GABARIT, + LARGEUR_ECHELLE_MM, + MENTIONS_GABARIT, + hauteursListes, + modeleDeLaPage, + pagesDuPlan, + renvois, +} from './plan.js'; + +const H = HAUTEUR_LIGNE_SUPPOSEE; +const TABLES = [...TABLES_APPLICATION, ...TABLES_INTERFACE]; + +// Le pied d'une feuille au tour donné, ou de la soirée. +const piedAu = (tour) => + feuilleDe({ nom: 'Soirée des Merles', date: null, tour, horodatage: null, version: 'epreuve' }, false); + +const demonstration = (cle) => analyser(texteLivre(cle)).charge; + +// L'état des places du tour 1 d'une charge, ses géométries à leur capacité. +function plan(charge, tour = 1) { + const etat = etatPlaces(charge, { tour }); + const geometries = etat.tables.map((table) => geometrieTable(table, table.capacite)); + return { etat, geometries }; +} + +// Une charge de tables rondes de 6 places, aux positions données (cm). +function chargeDeTables(positions) { + const charge = creerCharge({ id: 'epreuve:plan-imprime', nom: 'Plan imprimé', siegesParDefaut: 6, tours: 2 }); + charge.tables = positions.map(([x, y], i) => ({ id: i + 1, numero: i + 1, sieges: null, forme: 'ronde', position: { x, y } })); + charge.prochainsIds = { participant: 1, table: positions.length + 1, proposition: 1 }; + return charge; +} + +// La découpe attendue, par decouperPlan appelée directement. +function decoupeAttendue(etat, geometries, mode, { papier = 'A4', bandeMm = 12, listes = false, hauteurLigne = H } = {}) { + const blocs = listes ? { largeur: constante('LARGEUR_BLOC_NOMS_CM'), hauteurs: hauteursListes(etat, hauteurLigne) } : null; + return decouperPlan(englobantImpression(geometries, { listes: blocs }), { papier, bandeMm, mode, geometries, listes: blocs }); +} + +describe('hauteursListes : la hauteur du bloc de noms de chaque table (§ 7.4, § 7.7)', () => { + test('une ligne par siège, l’en-tête d’une table gelée, et une ligne pour le jambage de la dernière', () => { + const etat = { tables: [{ id: 4, capacite: 6, gelee: false }, { id: 9, capacite: 3, gelee: true }] }; + const hauteurs = hauteursListes(etat, 20); + assert.deepEqual([...hauteurs], [[4, 7 * 20], [9, 5 * 20]]); + }); + + test('une hauteur de ligne non finie ou ≤ 0 se refuse', () => { + const etat = { tables: [{ id: 1, capacite: 2, gelee: false }] }; + for (const h of [0, -1, Number.NaN, Number.POSITIVE_INFINITY, null]) { + assert.throws(() => hauteursListes(etat, h), RangeError, String(h)); + } + }); +}); + +describe('pagesDuPlan : une page par feuille, chacune la tuile de la découpe (§ 11.7)', () => { + for (const [mode, listes] of [ + ['auto', false], + ['auto', true], + ['decoupe', true], + ['unePage', true], + ]) { + test(`grande démonstration, ${mode}, listes ${listes ? 'montrées' : 'masquées'} : N pages, celles de decouperPlan, tuile, cartouche et tables`, () => { + const { etat, geometries } = plan(demonstration('grande')); + const attendue = decoupeAttendue(etat, geometries, mode, { listes }); + const pages = pagesDuPlan(etat, geometries, mode, { papier: 'A4', bandeMm: 12, listes, hauteurLigne: H, pied: piedAu(1) }); + assert.equal(pages.length, attendue.pages.length); + for (const [rang, page] of pages.entries()) { + const tuile = attendue.pages[rang]; + assert.equal(page.rang, tuile.rang); + assert.equal(page.orientation, attendue.orientation); + assert.deepEqual(page.viewBox, tuile.cadrage); + assert.deepEqual(page.cartouche, tuile.cartouche); + assert.deepEqual(page.tables, tuile.tables); + assert.deepEqual(page.taille, { largeur: tuile.cadrage.largeur * attendue.echelle, hauteur: tuile.cadrage.hauteur * attendue.echelle }); + } + }); + } + + test('le pied de chaque page est celui d’un seul tour, le tour imprimé', () => { + const { etat, geometries } = plan(demonstration('grande')); + const pied = piedAu(3); + const pages = pagesDuPlan(etat, geometries, 'decoupe', { papier: 'A4', bandeMm: 12, listes: true, hauteurLigne: H, pied }); + assert.ok(pages.length > 1); + for (const page of pages) { + assert.equal(page.pied, pied); + const tour = page.pied.elements.find(({ element }) => element === 'tour'); + assert.deepEqual(tour.libelle, { cle: 'impression.pied.tour', details: { tour: 3 } }); + } + }); + + test('un pied de la soirée se refuse : le plan s’imprime à un tour', () => { + const { etat, geometries } = plan(demonstration('petite')); + assert.throws(() => pagesDuPlan(etat, geometries, 'auto', { papier: 'A4', bandeMm: 12, pied: piedAu('tous') }), RangeError); + }); + + test('chaque page porte la mention « disposition relative » ; celle des listes, si et seulement si les listes sont montrées et que la page porte une table', () => { + // Deux tables à 200 m l'une de l'autre : découpé, le plan a des pages + // sans table entre elles. + const { etat, geometries } = plan(chargeDeTables([[0, 0], [20000, 0]])); + for (const listes of [true, false]) { + const pages = pagesDuPlan(etat, geometries, 'decoupe', { papier: 'A4', bandeMm: 12, listes, hauteurLigne: H, pied: piedAu(1) }); + const vides = pages.filter(({ tables }) => tables.length === 0); + assert.ok(vides.length > 0 && vides.length < pages.length, `${vides.length} pages vides sur ${pages.length}`); + for (const page of pages) { + assert.equal(page.mentions.relative, true); + assert.equal(page.mentions.listes, listes && page.tables.length > 0, `page ${page.rang}, listes ${listes}`); + } + } + }); + + test('l’échelle graphique de chaque page : la longueur ronde qui tient dans LARGEUR_ECHELLE_MM à l’échelle de la découpe, et sa largeur en mm', () => { + for (const cle of ['petite', 'grande']) { + const { etat, geometries } = plan(demonstration(cle)); + for (const mode of ['auto', 'unePage']) { + const attendue = decoupeAttendue(etat, geometries, mode, { listes: true }); + const pages = pagesDuPlan(etat, geometries, mode, { papier: 'A4', bandeMm: 12, listes: true, hauteurLigne: H, pied: piedAu(1) }); + const { longueurCm, largeurPx } = echelleGraphique(attendue.echelle, LARGEUR_ECHELLE_MM); + for (const page of pages) { + assert.deepEqual(page.echelle, { longueurCm, largeurMm: largeurPx }); + assert.ok(page.echelle.largeurMm <= LARGEUR_ECHELLE_MM); + assert.equal(page.echelle.largeurMm, page.echelle.longueurCm * attendue.echelle); + } + } + } + }); + + test('grande démonstration, listes montrées, A4 : découpée par défaut, chaque table sur une page au moins', () => { + const { etat, geometries } = plan(demonstration('grande')); + const pages = pagesDuPlan(etat, geometries, 'auto', { papier: 'A4', bandeMm: 12, listes: true, hauteurLigne: H, pied: piedAu(1) }); + assert.ok(pages.length > 1, `${pages.length} page(s)`); + const portees = new Set(pages.flatMap(({ tables }) => tables)); + assert.deepEqual([...portees].sort((a, b) => a - b), etat.tables.map(({ id }) => id).sort((a, b) => a - b)); + }); + + test('les listes montrées étendent la découpe de leurs blocs : sans hauteur de ligne, elles se refusent', () => { + const { etat, geometries } = plan(demonstration('petite')); + assert.throws(() => pagesDuPlan(etat, geometries, 'auto', { papier: 'A4', bandeMm: 12, listes: true, pied: piedAu(1) }), RangeError); + const sans = pagesDuPlan(etat, geometries, 'unePage', { papier: 'A4', bandeMm: 12, listes: false, pied: piedAu(1) }); + const avec = pagesDuPlan(etat, geometries, 'unePage', { papier: 'A4', bandeMm: 12, listes: true, hauteurLigne: H, pied: piedAu(1) }); + // Sur une page, le plan avec ses listes couvre plus de dessin : son + // échelle est plus petite. + assert.ok(avec[0].taille.largeur / avec[0].viewBox.largeur < sans[0].taille.largeur / sans[0].viewBox.largeur); + }); + + test('un plan vide : une page, aucune table, une échelle graphique finie', () => { + const { etat, geometries } = plan(chargeDeTables([])); + const pages = pagesDuPlan(etat, geometries, 'auto', { papier: 'Lettre', bandeMm: 12, listes: true, hauteurLigne: H, pied: piedAu(1) }); + assert.equal(pages.length, 1); + assert.deepEqual(pages[0].tables, []); + assert.equal(pages[0].mentions.listes, false); + assert.ok(Number.isFinite(pages[0].echelle.largeurMm) && pages[0].echelle.largeurMm > 0); + }); +}); + +describe('renvois, modeleDeLaPage et le gabarit de la bande', () => { + test('renvois : les pages voisines, haut, droite, bas, gauche, numérotées de 1, les bords omis', () => { + const cartouche = { page: 5, total: 9, colonne: 'B', rangee: 2, voisins: { haut: 1, droite: 5, bas: null, gauche: 3 } }; + assert.deepEqual(renvois(cartouche), [ + { sens: 'haut', cle: 'impression.plan.renvoi.haut', page: 2 }, + { sens: 'droite', cle: 'impression.plan.renvoi.droite', page: 6 }, + { sens: 'gauche', cle: 'impression.plan.renvoi.gauche', page: 4 }, + ]); + assert.deepEqual(renvois({ ...cartouche, voisins: { haut: null, droite: null, bas: null, gauche: null } }), []); + }); + + test('modeleDeLaPage : les tables de la page, dans l’ordre du modèle ; le reste du modèle tel quel', () => { + const charge = demonstration('petite'); + const etat = etatPlaces(charge, { tour: 1 }); + const modele = modeleDessin(etat, { listes: true, nomDe: () => 'Iris Ombrelle', impression: true }); + const ids = modele.tables.map(({ id }) => id); + const page = { tables: [ids[2], ids[0]] }; + const reduit = modeleDeLaPage(modele, page); + assert.deepEqual(reduit.tables.map(({ id }) => id), [ids[0], ids[2]]); + assert.equal(reduit.tables[0], modele.tables[0]); + assert.deepEqual({ ...reduit, tables: null }, { ...modele, tables: null }); + }); + + test('chaque renvoi a son libellé, en français comme en langue témoin', () => { + for (const traduction of [creerTraduction(TABLES), creerTraduction(TABLES, { langue: 'temoin' })]) { + for (const { cle, page } of renvois(CARTOUCHE_GABARIT)) { + assert.match(traduction.t(cle, { page }), /888|887/u, cle); + } + } + }); + + test('le gabarit porte chaque élément que la bande peut porter : les quatre renvois, les deux mentions, l’échelle', () => { + assert.equal(renvois(CARTOUCHE_GABARIT).length, 4); + assert.deepEqual(MENTIONS_GABARIT, { relative: true, listes: true }); + assert.ok(ECHELLE_GABARIT.largeurMm > 0 && ECHELLE_GABARIT.largeurMm <= LARGEUR_ECHELLE_MM); + }); +}); diff --git a/src/interface/libelles/impression.js b/src/interface/libelles/impression.js index c590332..2300e4f 100644 --- a/src/interface/libelles/impression.js +++ b/src/interface/libelles/impression.js @@ -51,8 +51,22 @@ export const TABLE = new Map([ ['impression.pied.placementInconnu', 'Placement non daté'], ['impression.pied.version', 'Version'], - // --- Les vues, avant leur mise en page (§ 11.7) ------------------------------ + // --- Le plan imprimé (§ 7.7, § 11.7) --------------------------------------- ['impression.plan.page', ({ page, total }) => `Plan, page ${page} sur ${total}`], + [ + 'impression.plan.cartouche', + ({ page, total, colonne, rangee }) => `Page ${page} sur ${total} \u{2014} colonne ${colonne}, rangée ${rangee}`, + ], + ['impression.plan.renvoi.haut', ({ page }) => `au-dessus : page ${page}`], + ['impression.plan.renvoi.droite', ({ page }) => `à droite : page ${page}`], + ['impression.plan.renvoi.bas', ({ page }) => `au-dessous : page ${page}`], + ['impression.plan.renvoi.gauche', ({ page }) => `à gauche : page ${page}`], + [ + 'impression.plan.listes', + "listes de noms hors d'échelle \u{2014} le plan à l'échelle du bâtiment s'imprime listes masquées", + ], + + // --- Les vues, avant leur mise en page (§ 11.7) ------------------------------ ['impression.accueil.titre', "Liste d'accueil"], ['impression.tables.titre', 'Feuilles par table'], ]); diff --git a/src/interface/plan/ListeNoms.svelte b/src/interface/plan/ListeNoms.svelte index 30b96a1..1d34d77 100644 --- a/src/interface/plan/ListeNoms.svelte +++ b/src/interface/plan/ListeNoms.svelte @@ -17,6 +17,13 @@ // Le texte est en white-space: pre, pour que les espaces des parties // comptent comme elles ont été mesurées : le gabarit n'a aucun blanc entre // les balises du . + // + // Le texte se compose en text-rendering: geometricPrecision, comme la + // sonde qui le mesure (PlanDessin) : le moteur de rendu le met alors en + // page à son corps du dessin, sans le recomposer au corps qu'il prend à + // l'écran. Sans lui, la largeur mesurée suit l'échelle d'affichage — le + // zoom du plan, l'échelle d'une page imprimée —, et une même ligne + // s'abrège à des longueurs différentes à l'écran et sur le papier (§ 7.4). import { getContext } from 'svelte'; import { TAILLE_TEXTE_LISTE_CM } from '../../geometrie/texte.js'; import { ajusterLigne, ordonneesLignes, partiesLigne, textesLigne } from './modele.js'; @@ -52,6 +59,7 @@ .liste { fill: var(--couleur-liste-texte); font-family: var(--police-interface); + text-rendering: geometricPrecision; white-space: pre; pointer-events: none; } diff --git a/src/interface/plan/PlanDessin.svelte b/src/interface/plan/PlanDessin.svelte index 47bc14f..0078f19 100644 --- a/src/interface/plan/PlanDessin.svelte +++ b/src/interface/plan/PlanDessin.svelte @@ -23,6 +23,10 @@ // taille du qui porte le dessin, qu'un ResizeObserver signale : un // plan qui se cache perd ses mesures, et ses listes, et les reprend en se // montrant. + // + // Le modèle du plan imprimé (modele.impression, § 11.7) se dessine de + // même, chaque table sans poignée, ni cible, ni identifiant (TablePlan) ; + // ses listes ont la largeur et la troncature de l'écran (§ 7.4). import { untrack } from 'svelte'; import { constante } from '../../geometrie/constantes.js'; import { TAILLE_TEXTE_LISTE_CM } from '../../geometrie/texte.js'; @@ -60,7 +64,7 @@ {#each modele.tables as table (table.id)} - + {/each} de la page porte le + // sien, et son contenu est caché aux technologies d'assistance —, ni + // poignée, ni cible. Le plateau, les chaises, les marques, les trames et + // la pastille sont ceux de l'écran : le noir et blanc se lit par la forme. import { getContext } from 'svelte'; import { RAYON_POIGNEE } from '../../geometrie/tables.js'; import { @@ -56,7 +62,7 @@ texteLigne, } from './modele.js'; - let { table, signaler, prefixe } = $props(); + let { table, signaler, prefixe, impression = false } = $props(); const t = getContext('t'); @@ -78,7 +84,12 @@ } - + {#if table.regime === 'table'} {#if formes.ronde} @@ -116,10 +127,10 @@ {/if} {#each table.chaises as chaise (chaise.siege)} {/if} - + {#if !impression} + + {/if} {/each} - +{#if !impression} + +{/if} {#if table.pastille > 0} diff --git a/src/interface/plan/modele.js b/src/interface/plan/modele.js index 7dcb8ad..38db65f 100644 --- a/src/interface/plan/modele.js +++ b/src/interface/plan/modele.js @@ -314,12 +314,13 @@ function porteurDe(place, id, regime) { } // Le dessin d'une table de l'état des places au zoom k ; places, le nombre -// de sièges que la poignée montre, sinon sa capacité. -function tableDessin(etatTable, { k, miseEnEvidence, nomDe, selection = null, actif = null }, places = etatTable.capacite) { +// de sièges que la poignée montre, sinon sa capacité. Imprimée, la table +// est en régime sièges, quel que soit k. +function tableDessin(etatTable, { k, miseEnEvidence, nomDe, selection = null, actif = null, impression = false }, places = etatTable.capacite) { const { id, numero, gelee, surchargee, conflits } = etatTable; const geometrie = geometrieTable(etatTable, places); const trames = conflits === null ? [] : conflits.regles.map(({ regle }) => ({ ...SORTE_DE_REGLE.get(regle) })); - const regime = regimeCibles(k, geometrie); + const regime = impression ? 'sieges' : regimeCibles(k, geometrie); const pastille = conflits === null ? 0 : conflits.nombre; const etatChaises = chaisesA(etatTable, places); const selectionnee = porteurDe(selection, id, regime); @@ -390,24 +391,34 @@ function tableDessin(etatTable, { k, miseEnEvidence, nomDe, selection = null, ac * accessible —, ses chaises 1 à places, celles au-delà de sa capacité * libres ; l'état des places n'est pas touché, et rien ne s'écrit (§ 7.1). * + * L'option impression rend le plan imprimé (§ 11.7) : k n'y entre pas, + * chaque table est en régime sièges, ses chaises de 1 à sa capacité, et les + * listes se rendent dès qu'elles sont demandées. Le papier ne reçoit aucun + * geste : poignee, selection, actif et miseEnEvidence sont sans effet, et + * aucun porteur n'est marqué. Le modèle porte impression, que le dessin lit + * pour ne poser ni poignée, ni cible, ni identifiant, ni rôle. + * * @param {Object} etatPlaces EtatPlaces (A5) - * @param {{k: number, listes: boolean, miseEnEvidence: number|null, + * @param {{k?: number, listes: boolean, miseEnEvidence?: number|null, * nomDe: (id: number) => string, diagnostic?: Object|null, * poignee?: {table: number, places: number}|null, * selection?: {table: number, siege?: number|null}|null, - * actif?: {table: number, siege: number|null}|null}} options - * diagnostic : celui que rend diagnostiquer, compléments de M1 compris + * actif?: {table: number, siege: number|null}|null, + * impression?: boolean}} options + * diagnostic : celui que rend diagnostiquer, compléments de M1 compris ; + * k : exigé hors de l'impression ; miseEnEvidence : null par défaut * @returns {Object} ModeleDessin */ export function modeleDessin( etatPlaces, - { k, listes, miseEnEvidence, nomDe, diagnostic = null, poignee = null, selection = null, actif = null }, + { k, listes, miseEnEvidence = null, nomDe, diagnostic = null, poignee = null, selection = null, actif = null, impression = false }, ) { + const gestes = impression ? { miseEnEvidence: null, selection: null, actif: null } : { miseEnEvidence, selection, actif }; const tables = etatPlaces.tables.map((etatTable) => tableDessin( etatTable, - { k, miseEnEvidence, nomDe, selection, actif }, - poignee !== null && poignee.table === etatTable.id ? poignee.places : etatTable.capacite, + { k, nomDe, impression, ...gestes }, + !impression && poignee !== null && poignee.table === etatTable.id ? poignee.places : etatTable.capacite, ), ); const toutesEnConflit = tables.length > 0 && tables.every(({ conflit }) => conflit); @@ -415,7 +426,8 @@ export function modeleDessin( tables, signalerConflits: !toutesEnConflit, bandeau: bandeauDe(toutesEnConflit, diagnostic), - listesRendues: listes && k >= constante('K_LISTES'), + listesRendues: listes && (impression || k >= constante('K_LISTES')), + impression, }; } diff --git a/src/interface/plan/modele.test.js b/src/interface/plan/modele.test.js index 66a206d..5172bfd 100644 --- a/src/interface/plan/modele.test.js +++ b/src/interface/plan/modele.test.js @@ -1217,3 +1217,92 @@ describe('les clés que les panneaux composent existent (§ 14.6)', () => { } }); }); + +describe('modeleDessin, option impression : le plan imprimé (§ 11.7, § 13.5)', () => { + // La grande démonstration au tour 1, sa proposition montrée : des tables + // de toutes tailles, dont le régime à l'écran dépend de k. + const grande = () => analyser(texteLivre('grande')).charge; + + test('régime des sièges pour chaque table, quel que soit k ; chaque chaise dessinée, de 1 à la capacité', () => { + const charge = grande(); + const etat = etatPlaces(charge, { tour: 1 }); + // À l'écran, au plus petit zoom, aucune chaise ne se dessine. + const ecran = modeleDessin(etat, options(charge, { k: 0.01 })); + assert.ok(ecran.tables.every(({ regime }) => regime === 'table')); + for (const k of [undefined, 0.01, 5]) { + const modele = modeleDessin(etat, options(charge, { k, impression: true })); + assert.equal(modele.tables.length, etat.tables.length); + for (const [rang, table] of modele.tables.entries()) { + assert.equal(table.regime, 'sieges', `table ${table.id}, k = ${String(k)}`); + assert.deepEqual( + table.chaises.map(({ siege }) => siege), + etat.tables[rang].chaises.map(({ siege }) => siege), + ); + } + } + }); + + test('ni sélection, ni focus, ni mise en évidence : les options de l’écran sont sans effet', () => { + const charge = avecPropositions(petite()); + const etat = etatPlaces(charge, { tour: 1, proposition: charge.propositions[0].id }); + const [premiere, deuxieme] = etat.tables; + const occupant = premiere.chaises.find((chaise) => chaise.occupant !== null).occupant; + const ecran = { + selection: { table: premiere.id, siege: 1 }, + actif: { table: deuxieme.id, siege: null }, + miseEnEvidence: occupant, + poignee: { table: premiere.id, places: 1 }, + }; + // À l'écran, ces options marquent leur porteur. + const marque = modeleDessin(etat, options(charge, { k: 5, ...ecran })); + assert.ok(marque.tables.some((table) => table.focus || table.chaises.some((chaise) => chaise.selection || chaise.miseEnEvidence))); + const modele = modeleDessin(etat, options(charge, { impression: true, ...ecran })); + for (const table of modele.tables) { + assert.equal(table.selection, false); + assert.equal(table.focus, false); + for (const chaise of table.chaises) { + assert.deepEqual([chaise.selection, chaise.focus, chaise.miseEnEvidence], [false, false, false], `table ${table.id}`); + } + } + // La poignée ne retire aucune chaise au papier : la table tenue garde sa capacité. + assert.deepEqual( + modele.tables[0].chaises.map(({ siege }) => siege), + Array.from({ length: premiere.capacite }, (_, rang) => rang + 1), + ); + }); + + test('les listes se rendent quand elles sont demandées, sans égard à K_LISTES', () => { + const charge = avecPropositions(petite()); + const etat = etatPlaces(charge, { tour: 1 }); + assert.equal(modeleDessin(etat, options(charge, { k: K_LISTES - 0.01, impression: true })).listesRendues, true); + assert.equal(modeleDessin(etat, options(charge, { impression: true })).listesRendues, true); + assert.equal(modeleDessin(etat, options(charge, { impression: true, listes: false })).listesRendues, false); + }); + + test('le modèle se dit imprimé, et l’écran ne l’est pas : le dessin y lit l’absence de poignée, d’identifiant et de cible', () => { + const charge = avecPropositions(petite()); + const etat = etatPlaces(charge, { tour: 1 }); + assert.equal(modeleDessin(etat, options(charge, { impression: true })).impression, true); + assert.equal(modeleDessin(etat, options(charge)).impression, false); + }); + + test('les conflits, les pastilles, les marques et les lignes sont ceux de l’écran', () => { + const charge = avecPropositions(petite('petite-conflit')); + const etat = etatPlaces(charge, { tour: 1, proposition: charge.propositions[0].id }); + const ecran = modeleDessin(etat, options(charge, { k: 5 })); + const imprime = modeleDessin(etat, options(charge, { impression: true })); + assert.ok(imprime.tables.some(({ conflit }) => conflit), 'une table en conflit au moins'); + assert.equal(imprime.signalerConflits, ecran.signalerConflits); + for (const [rang, table] of imprime.tables.entries()) { + const voisine = ecran.tables[rang]; + assert.deepEqual( + [table.conflit, table.pastille, table.trames, table.lignes, table.enTete], + [voisine.conflit, voisine.pastille, voisine.trames, voisine.lignes, voisine.enTete], + ); + assert.deepEqual( + table.chaises.map(({ remplissage, marques }) => [remplissage, marques]), + voisine.chaises.map(({ remplissage, marques }) => [remplissage, marques]), + ); + } + }); +}); diff --git a/src/interface/transversal.navigateur.test.js b/src/interface/transversal.navigateur.test.js index cadc379..9cae75c 100644 --- a/src/interface/transversal.navigateur.test.js +++ b/src/interface/transversal.navigateur.test.js @@ -723,6 +723,26 @@ describe('les gardes relèvent la faute qu’un fragment porte (§ 14.2)', () => expect(noye.marques.fautes[0]).toContain('1.00:1'); }); + test('marques : sous le document imprimé, une marque se mesure au jeton du papier — celui du clair, dans les deux thèmes —, non à celui de la page', () => { + const racine = document.documentElement; + const theme = racine.getAttribute('data-theme'); + racine.setAttribute('data-theme', 'sombre'); + try { + const { vues, fautes, mesures } = avecFragment( + '
' + + '' + + '
', + (c) => releverMarques(c), + ); + expect(vues).toEqual(['contourTable']); + expect(fautes).toEqual([]); + expect(mesures).toBe(1); + } finally { + if (theme === null) racine.removeAttribute('data-theme'); + else racine.setAttribute('data-theme', theme); + } + }); + test('marques sous une fenêtre modale : une marque HTML de la page, qui ne s’y mesure pas, ne compte pas comme vue ; une marque dans la fenêtre, si', () => { const { vues } = avecFragment( '

dehors

' + diff --git a/test/navigateur/captures.js b/test/navigateur/captures.js index 73b7f46..2042a19 100644 --- a/test/navigateur/captures.js +++ b/test/navigateur/captures.js @@ -231,11 +231,13 @@ export function releverTextes(racine, { fenetre = false } = {}) { // --- Les marques ------------------------------------------------------------------- -/** La couleur résolue d'un jeton dans le thème courant. */ -export function couleurDeJeton(jeton) { +/** La couleur résolue d'un jeton dans le thème courant, ou sous hote : un + * élément qui redéclare les jetons — le document imprimé de l'aperçu, + * [data-impression], porte ceux du clair dans les deux thèmes (§ 11.7). */ +export function couleurDeJeton(jeton, hote = document.body) { const sonde = document.createElement('span'); sonde.style.color = `var(${jeton})`; - document.body.append(sonde); + hote.append(sonde); try { return composantes(getComputedStyle(sonde).color); } finally { @@ -363,7 +365,9 @@ function voisinage(element, jeton) { /** * Les marques rendues sous racine (§ 14.5, § 19.5) : chacune nommée au * registre, peinte de son jeton, à 3:1 de ce qui l'entoure, sans - * atténuation. → { vues: string[], fautes, attenuations, mesures, ratios }, + * atténuation. Sous le document imprimé de l'aperçu, le jeton se résout + * comme le papier le résout : au clair, dans les deux thèmes. + * → { vues: string[], fautes, attenuations, mesures, ratios }, * ratios : { marque, jeton, ratio } de chaque marque mesurée, contre ce qui * l'entoure de plus défavorable, dans l'ordre des mesures. fenetre vrai : * seules les marques que la fenêtre montre entières se lisent, sans rien @@ -397,7 +401,7 @@ export function releverMarques(racine, { fenetre = false } = {}) { if (modale !== null && !(element instanceof SVGElement) && !modale.contains(element)) continue; vues.add(nom); if (!fenetre) amener(element); - const jeton = couleurDeJeton(entree.jeton); + const jeton = couleurDeJeton(entree.jeton, element.closest('[data-impression]') ?? document.body); try { const { peinture, voisins } = voisinage(element, jeton); if (peinture === null) { diff --git a/test/navigateur/designations.js b/test/navigateur/designations.js index de6bf58..c7f2a62 100644 --- a/test/navigateur/designations.js +++ b/test/navigateur/designations.js @@ -29,6 +29,7 @@ const ENTREES = [ ['data-bandeau', 'le bandeau d’application', ['']], ['data-bandeau-plan', 'plan : le bandeau au-dessus de la surface', ['']], ['data-bloc', 'à propos : le bloc de version à copier', ['']], + ['data-cartouche', 'aperçu : le cartouche de repérage d’une page du plan imprimé', ['']], ['data-cellule', 'import : une cellule de l’aperçu, par son champ', null], ['data-champ', 'un champ de saisie ou un sélecteur, par son nom', null], ['data-chargement', 'accueil : le signe d’un calcul en cours', ['']], @@ -105,6 +106,7 @@ const ENTREES = [ ['data-meilleure-id', 'l’identifiant de la meilleure proposition', ['']], ['data-meilleure-libelle-id', 'génération : le libellé de l’identifiant de la meilleure', ['']], ['data-meilleure-vide', 'génération : la meilleure absente', ['']], + ['data-mention', 'aperçu : une mention d’une page du plan imprimé — disposition relative, listes hors d’échelle', ['listes', 'relative']], ['data-menu', 'plan : le menu d’une personne', null], ['data-mode', 'événement : le cadre de mode, par sa forme', null], ['data-mode-libelle', 'le libellé de mode', ['bandeau', 'cadre']],