[ADD] plan: one hue and pattern per conflict rule, legend, appartenance
The plan drew every conflict in one hue, so a table that seats colleagues together looked like one where people meet again. Each active rule now has its own reserved hue and pattern: same appartenance hatched one way, met again hatched the other, back at a table dotted; rules on one table overlap, the badge keeps the total. A legend from the view bar shows each active rule's sample. A table's tooltip names the appartenance that brings a pair together. The hues are validated by computation in both themes and the spec (7.5, 12.7) follows. Checked: 2848 node, 580 browser tests. --- FR --- [ADD] plan : une teinte et une trame par règle, légende, appartenance Le plan dessinait tout conflit d'une seule teinte : une table qui réunit des collègues ressemblait à une table où des gens se revoient. Chaque règle active a sa teinte réservée et sa trame : même appartenance hachurée dans un sens, voisins revus dans l'autre, table revue en pointillé ; les règles d'une table se superposent, la pastille garde le total. Une légende dans la barre de vue montre l'échantillon de chaque règle active. L'infobulle nomme l'appartenance qui réunit une paire. Teintes validées par calcul dans les deux thèmes ; spec § 7.5, § 12.7. Vérifié : 2848, 580. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
5f62698c82
commit
8afa53ebba
22 changed files with 1203 additions and 165 deletions
25
spec.md
25
spec.md
|
|
@ -1057,6 +1057,24 @@ Le signal **ne repose pas sur la seule couleur** : contour épaissi, trame, et
|
||||||
une **pastille portant le nombre de violations**. Une part notable des
|
une **pastille portant le nombre de violations**. Une part notable des
|
||||||
opérateurs ne distingue pas un rouge d'un vert.
|
opérateurs ne distingue pas un rouge d'un vert.
|
||||||
|
|
||||||
|
**Chaque règle a sa trame, d'une forme et d'une teinte propres** — hachures
|
||||||
|
descendantes pour « séparer les appartenances », hachures montantes pour
|
||||||
|
« nouveaux voisins », points pour « nouvelle table ». La forme suffit sans la
|
||||||
|
couleur ; la teinte la redit. Une table qui viole plusieurs règles superpose
|
||||||
|
leurs trames — les deux hachures se croisent en quadrillage — sous un même
|
||||||
|
contour épaissi, commun aux trois règles ; la pastille compte toutes ses
|
||||||
|
violations.
|
||||||
|
|
||||||
|
**Une légende des conflits reste accessible depuis le plan**, par la barre de
|
||||||
|
vue : pour chaque règle active, un échantillon de sa trame — la forme et la
|
||||||
|
teinte que le plan peint — et son nom ; elle dit aussi ce que compte la
|
||||||
|
pastille. Une règle inactive n'y paraît pas.
|
||||||
|
|
||||||
|
Le détail d'une table en conflit nomme les personnes de chaque violation ;
|
||||||
|
pour « séparer les appartenances », il nomme aussi **l'appartenance qui les
|
||||||
|
réunit**, telle que la liste la porte : « Même appartenance (Club des Merles) :
|
||||||
|
Iris Ombrelle et Théo Pervenche ».
|
||||||
|
|
||||||
**Quand toutes les tables sont en conflit, le plan cesse de signaler quoi que ce
|
**Quand toutes les tables sont en conflit, le plan cesse de signaler quoi que ce
|
||||||
soit** — cas que le diagnostic sait prédire. La pastille chiffrée garde sa
|
soit** — cas que le diagnostic sait prédire. La pastille chiffrée garde sa
|
||||||
valeur puisqu'elle ordonne les tables entre elles, et le bandeau énonce le
|
valeur puisqu'elle ordonne les tables entre elles, et le bandeau énonce le
|
||||||
|
|
@ -2603,8 +2621,11 @@ Une page de statistiques se casse par ses bords, et chacun se produira.
|
||||||
- **L'identité d'une série ne repose jamais sur la seule couleur** : au plus
|
- **L'identité d'une série ne repose jamais sur la seule couleur** : au plus
|
||||||
deux séries, **étiquetées directement sur le tracé** en plus de la légende. La
|
deux séries, **étiquetées directement sur le tracé** en plus de la légende. La
|
||||||
page s'imprime, souvent en noir et blanc.
|
page s'imprime, souvent en noir et blanc.
|
||||||
- **Les couleurs d'état sont réservées** : la teinte qui signale un conflit ici
|
- **Les couleurs d'état sont réservées** : **les teintes de conflit** — trois,
|
||||||
est **la même** que sur le plan de salle, et ne sert à rien d'autre.
|
une par règle que le plan localise (§ 7.5) — sont ici **les mêmes** que sur
|
||||||
|
le plan de salle, et ne servent à rien d'autre ; chacune ne peint que les
|
||||||
|
marques de sa règle. Elles se distinguent entre elles et des autres marques
|
||||||
|
du plan **une fois les trois dichromaties simulées**.
|
||||||
- **Une échelle séquentielle est une seule teinte, du clair au foncé.** Jamais
|
- **Une échelle séquentielle est une seule teinte, du clair au foncé.** Jamais
|
||||||
d'arc-en-ciel : un dégradé multicolore invente des frontières là où la
|
d'arc-en-ciel : un dégradé multicolore invente des frontières là où la
|
||||||
grandeur est continue.
|
grandeur est continue.
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,16 @@
|
||||||
les trois dichromaties. Aucun jeton de texte ou de marque n'a de canal
|
les trois dichromaties. Aucun jeton de texte ou de marque n'a de canal
|
||||||
alpha : la couleur relue dans le style calculé est alors la couleur peinte.
|
alpha : la couleur relue dans le style calculé est alors la couleur peinte.
|
||||||
|
|
||||||
La teinte de --couleur-conflit est réservée (§ 12.7) : tout autre jeton
|
Trois teintes sont réservées aux conflits (§ 7.5, § 12.7), une par règle
|
||||||
chromatique s'en tient à 30° au moins, et aucun ne porte sa valeur.
|
que le plan localise : --couleur-conflit-appartenance (séparer les
|
||||||
|
appartenances), --couleur-conflit-voisins (nouveaux voisins) et
|
||||||
|
--couleur-conflit-table (nouvelle table). Chacune ne peint que la trame de
|
||||||
|
sa règle ; tout autre jeton chromatique se tient à 30° au moins de chacune,
|
||||||
|
et aucun ne porte la valeur de l'une. Elles se distinguent entre elles et
|
||||||
|
des autres marques du plan sous les trois dichromaties (SEPARATIONS,
|
||||||
|
marques.js), et tiennent 3:1 sur le fond du plateau, qui borde leurs
|
||||||
|
traits. Le contour épaissi d'une table en conflit, commun aux trois, est
|
||||||
|
de l'encre forte du plan, --couleur-contour-fort.
|
||||||
|
|
||||||
La hachure d'une place à pourvoir peint des traits --couleur-hachure sur le
|
La hachure d'une place à pourvoir peint des traits --couleur-hachure sur le
|
||||||
fond de la chaise, --couleur-chaise-fond : ce sont ses deux couleurs. */
|
fond de la chaise, --couleur-chaise-fond : ce sont ses deux couleurs. */
|
||||||
|
|
@ -91,7 +99,10 @@
|
||||||
--couleur-chaise-pleine: #1f1f1f;
|
--couleur-chaise-pleine: #1f1f1f;
|
||||||
--couleur-hachure: #1f1f1f;
|
--couleur-hachure: #1f1f1f;
|
||||||
--couleur-marque: #767676;
|
--couleur-marque: #767676;
|
||||||
--couleur-conflit: #b3261e;
|
--couleur-contour-fort: #2b2f36;
|
||||||
|
--couleur-conflit-appartenance: #8f1d14;
|
||||||
|
--couleur-conflit-voisins: #c0189f;
|
||||||
|
--couleur-conflit-table: #6a9400;
|
||||||
--couleur-pastille-fond: #2b2f36;
|
--couleur-pastille-fond: #2b2f36;
|
||||||
--couleur-pastille-texte: #ffffff;
|
--couleur-pastille-texte: #ffffff;
|
||||||
--couleur-poignee: #4a5059;
|
--couleur-poignee: #4a5059;
|
||||||
|
|
@ -133,7 +144,10 @@
|
||||||
--couleur-chaise-pleine: #e8e8e8;
|
--couleur-chaise-pleine: #e8e8e8;
|
||||||
--couleur-hachure: #ededed;
|
--couleur-hachure: #ededed;
|
||||||
--couleur-marque: #7a7a7a;
|
--couleur-marque: #7a7a7a;
|
||||||
--couleur-conflit: #ff8a80;
|
--couleur-contour-fort: #e6e8eb;
|
||||||
|
--couleur-conflit-appartenance: #fc9890;
|
||||||
|
--couleur-conflit-voisins: #c0689c;
|
||||||
|
--couleur-conflit-table: #549c00;
|
||||||
--couleur-pastille-fond: #e6e8eb;
|
--couleur-pastille-fond: #e6e8eb;
|
||||||
--couleur-pastille-texte: #15181c;
|
--couleur-pastille-texte: #15181c;
|
||||||
--couleur-poignee: #c5ccd6;
|
--couleur-poignee: #c5ccd6;
|
||||||
|
|
|
||||||
|
|
@ -113,15 +113,38 @@ export const TABLE = new Map([
|
||||||
// Le plan est un seul arrêt de tabulation : sa description dit comment s'y
|
// Le plan est un seul arrêt de tabulation : sa description dit comment s'y
|
||||||
// déplacer. L'infobulle d'une table en conflit nomme chaque règle violée,
|
// déplacer. L'infobulle d'une table en conflit nomme chaque règle violée,
|
||||||
// puis ses groupes : les noms sont des données, chacun dans son nœud,
|
// puis ses groupes : les noms sont des données, chacun dans son nœud,
|
||||||
// reliés par « et » dans une paire et séparés d'un groupe à l'autre.
|
// reliés par « et » dans une paire et séparés d'un groupe à l'autre. Une
|
||||||
|
// ligne de séparer les appartenances nomme l'appartenance de ses paires,
|
||||||
|
// une donnée dans son nœud, entre separerAppartenances et finAppartenance.
|
||||||
['plan.vue.role', 'plan de salle interactif'],
|
['plan.vue.role', 'plan de salle interactif'],
|
||||||
['plan.infobulle.entete', ({ numero, nombre }) => `Table ${numero} : ${compte(nombre, 'conflit', 'conflits')}`],
|
['plan.infobulle.entete', ({ numero, nombre }) => `Table ${numero} : ${compte(nombre, 'conflit', 'conflits')}`],
|
||||||
['plan.infobulle.separerAppartenances', 'appartenances réunies : '],
|
['plan.infobulle.separerAppartenances', 'Même appartenance ('],
|
||||||
|
['plan.infobulle.finAppartenance', ') : '],
|
||||||
['plan.infobulle.nouveauxVoisins', 'voisins revus : '],
|
['plan.infobulle.nouveauxVoisins', 'voisins revus : '],
|
||||||
['plan.infobulle.nouvelleTable', 'table revue : '],
|
['plan.infobulle.nouvelleTable', 'table revue : '],
|
||||||
['plan.infobulle.et', ' et '],
|
['plan.infobulle.et', ' et '],
|
||||||
['plan.infobulle.separateur', ', '],
|
['plan.infobulle.separateur', ', '],
|
||||||
|
|
||||||
|
// --- La légende des conflits (§ 7.5) --------------------------------------
|
||||||
|
// Une entrée par règle active : son échantillon, son nom, puis la forme de
|
||||||
|
// sa trame, qui la distingue sans la couleur. L'exemple de la pastille est
|
||||||
|
// le chiffre que porte son échantillon.
|
||||||
|
['plan.legende.ouvrir', 'Légende'],
|
||||||
|
['plan.legende.titre', 'Légende des conflits'],
|
||||||
|
['plan.legende.separerAppartenances', 'Séparer les appartenances : même appartenance à la table'],
|
||||||
|
['plan.legende.nouveauxVoisins', 'Nouveaux voisins à chaque tour : voisins revus'],
|
||||||
|
['plan.legende.nouvelleTable', 'Nouvelle table à chaque tour : table revue'],
|
||||||
|
['plan.legende.forme.hachuresDescendantes', '(hachures descendantes)'],
|
||||||
|
['plan.legende.forme.hachuresMontantes', '(hachures montantes)'],
|
||||||
|
['plan.legende.forme.points', '(points)'],
|
||||||
|
['plan.legende.aucune', 'Aucune règle de conflit n\u{2019}est active.'],
|
||||||
|
[
|
||||||
|
'plan.legende.superposition',
|
||||||
|
'Une table qui viole plusieurs règles superpose leurs trames ; son contour épaissi la signale quelle que soit la règle.',
|
||||||
|
],
|
||||||
|
['plan.legende.pastille', 'Pastille : le nombre de conflits de la table à ce tour, toutes règles confondues.'],
|
||||||
|
['plan.legende.pastilleExemple', '2'],
|
||||||
|
|
||||||
// --- Les panneaux latéraux (§ 2.1, étape 8) --------------------------------
|
// --- Les panneaux latéraux (§ 2.1, étape 8) --------------------------------
|
||||||
['plan.panneaux', 'Panneaux du plan'],
|
['plan.panneaux', 'Panneaux du plan'],
|
||||||
['plan.listesTravail.titre', 'Listes de travail'],
|
['plan.listesTravail.titre', 'Listes de travail'],
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,38 @@ function entree(nom, jeton, fonds, seuil) {
|
||||||
const marque = (nom, jeton, fonds) => entree(nom, jeton, fonds, 3);
|
const marque = (nom, jeton, fonds) => entree(nom, jeton, fonds, 3);
|
||||||
const texte = (nom, jeton, fonds) => entree(nom, jeton, fonds, 4.5);
|
const texte = (nom, jeton, fonds) => entree(nom, jeton, fonds, 4.5);
|
||||||
|
|
||||||
|
// Les sortes de conflit : règle, marque de la trame, jeton court, forme.
|
||||||
|
const SORTES_CONFLIT = [
|
||||||
|
['separerAppartenances', 'trameAppartenance', 'conflit-appartenance', 'hachuresDescendantes'],
|
||||||
|
['nouveauxVoisins', 'trameVoisins', 'conflit-voisins', 'hachuresMontantes'],
|
||||||
|
['nouvelleTable', 'trameTable', 'conflit-table', 'points'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Les trois sortes de conflit qu'une table porte (§ 7.5, § 12.7), une par
|
||||||
|
* règle que le moteur localise (conflitsParTable), dans son ordre — l'ordre
|
||||||
|
* du dessin, de la légende et de l'infobulle : { regle, marque, jeton,
|
||||||
|
* forme } — la règle du moteur, la marque data-marque de sa trame, le jeton
|
||||||
|
* de sa teinte, réservée à cette trame, et la forme de son motif
|
||||||
|
* (Motifs.svelte), qui la distingue sans la couleur. Deux règles violées à
|
||||||
|
* une même table superposent leurs trames : les hachures descendantes et
|
||||||
|
* montantes se croisent en quadrillage, les points se posent par-dessus. */
|
||||||
|
export const CONFLITS = Object.freeze(
|
||||||
|
SORTES_CONFLIT.map(([regle, trame, jeton, forme]) => Object.freeze({ regle, marque: trame, jeton: c(jeton), forme })),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Les jetons des teintes de conflit, dans l'ordre de CONFLITS. */
|
||||||
|
export const JETONS_CONFLIT = Object.freeze(CONFLITS.map(({ jeton }) => jeton));
|
||||||
|
|
||||||
|
/** Écarts perçus minimaux d'une teinte de conflit, en distance OKLab × 100
|
||||||
|
* mesurée en couleurs d'origine et sous chaque dichromatie (ecartPercu,
|
||||||
|
* palette.js) : entreConflits, d'une teinte de conflit à une autre — la
|
||||||
|
* couleur redit la forme de la règle ; autresMarques, d'une teinte de
|
||||||
|
* conflit à chaque autre marque du plan. Le second seuil est plus bas : les
|
||||||
|
* gris du mobilier occupent la clarté où une teinte de conflit tient 3:1
|
||||||
|
* sur le plateau sombre, et la forme distingue toujours la trame de ce
|
||||||
|
* qu'elle côtoie. */
|
||||||
|
export const SEPARATIONS = Object.freeze({ entreConflits: 12, autresMarques: 5 });
|
||||||
|
|
||||||
/** Les marques porteuses d'information (§ 14.5) : { nom, jeton, fonds, seuil: 3 } — nom, la
|
/** Les marques porteuses d'information (§ 14.5) : { nom, jeton, fonds, seuil: 3 } — nom, la
|
||||||
* désignation data-marque ; jeton, celui qui la peint ; fonds, chaque jeton sur lequel elle
|
* désignation data-marque ; jeton, celui qui la peint ; fonds, chaque jeton sur lequel elle
|
||||||
* se peint, chaque remplissage d'une chaise pour une marque qui passe sur une chaise. */
|
* se peint, chaque remplissage d'une chaise pour une marque qui passe sur une chaise. */
|
||||||
|
|
@ -34,10 +66,12 @@ export const MARQUES = Object.freeze([
|
||||||
marque('anneau', 'marque', ['sol', 'table-fond', ...REMPLISSAGES_CHAISE]),
|
marque('anneau', 'marque', ['sol', 'table-fond', ...REMPLISSAGES_CHAISE]),
|
||||||
marque('hachure', 'hachure', ['chaise-fond']),
|
marque('hachure', 'hachure', ['chaise-fond']),
|
||||||
marque('remplissagePlein', 'chaise-pleine', ['sol', 'table-fond']),
|
marque('remplissagePlein', 'chaise-pleine', ['sol', 'table-fond']),
|
||||||
// Le conflit (§ 7.5) : contour épaissi sur le sol, trame sur le plateau,
|
// Le conflit (§ 7.5) : contour épaissi sur le sol, commun aux trois
|
||||||
// pastille chiffrée près de la table.
|
// règles et peint de l'encre forte du plan ; une trame par règle violée
|
||||||
marque('contourConflit', 'conflit', ['sol', 'table-fond']),
|
// sur le plateau, chacune de sa teinte, ses traits détachés par un halo
|
||||||
marque('trameConflit', 'conflit', ['table-fond']),
|
// du fond du plateau ; pastille chiffrée près de la table.
|
||||||
|
marque('contourConflit', 'contour-fort', ['sol', 'table-fond']),
|
||||||
|
...SORTES_CONFLIT.map(([, trame, jeton]) => marque(trame, jeton, ['table-fond'])),
|
||||||
marque('pastille', 'pastille-fond', ['sol', 'table-fond']),
|
marque('pastille', 'pastille-fond', ['sol', 'table-fond']),
|
||||||
marque('poignee', 'poignee', ['sol']),
|
marque('poignee', 'poignee', ['sol']),
|
||||||
marque('gel', 'gel', ['sol', 'table-fond']),
|
marque('gel', 'gel', ['sol', 'table-fond']),
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { describe, test } from '../../test/lanceur.js';
|
import { describe, test } from '../../test/lanceur.js';
|
||||||
import { MARQUES, TEXTES } from './marques.js';
|
import { CONFLITS, JETONS_CONFLIT, MARQUES, SEPARATIONS, TEXTES } from './marques.js';
|
||||||
import { lireBlocs, lireJetons, verifierPalette } from './palette.js';
|
import { lireBlocs, lireJetons, verifierPalette, verifierSeparation } from './palette.js';
|
||||||
|
|
||||||
const JETONS = readFileSync(new URL('./jetons.css', import.meta.url), 'utf8');
|
const JETONS = readFileSync(new URL('./jetons.css', import.meta.url), 'utf8');
|
||||||
const THEMES = ['clair', 'sombre'];
|
const THEMES = ['clair', 'sombre'];
|
||||||
|
|
||||||
const NOMS_MARQUES = [
|
const NOMS_MARQUES = [
|
||||||
'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein',
|
'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein',
|
||||||
'contourConflit', 'trameConflit', 'pastille', 'poignee', 'gel', 'miseEnEvidence',
|
'contourConflit', 'trameAppartenance', 'trameVoisins', 'trameTable', 'pastille', 'poignee', 'gel', 'miseEnEvidence',
|
||||||
'selection', 'cadreLecture', 'cadreEcriture', 'cadreBloque', 'focus',
|
'selection', 'cadreLecture', 'cadreEcriture', 'cadreBloque', 'focus',
|
||||||
'contourTable', 'contourChaise', 'echelle', 'fantome',
|
'contourTable', 'contourChaise', 'echelle', 'fantome',
|
||||||
];
|
];
|
||||||
|
|
@ -39,7 +39,7 @@ const FONDS_REQUIS = {
|
||||||
const parNom = (registre, nom) => registre.find((e) => e.nom === nom);
|
const parNom = (registre, nom) => registre.find((e) => e.nom === nom);
|
||||||
|
|
||||||
describe('registre des marques', () => {
|
describe('registre des marques', () => {
|
||||||
test('les vingt marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
|
test('les vingt-deux marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
|
||||||
assert.equal(MARQUES.length, NOMS_MARQUES.length);
|
assert.equal(MARQUES.length, NOMS_MARQUES.length);
|
||||||
assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES);
|
assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES);
|
||||||
assert.equal(new Set(MARQUES.map((m) => m.nom)).size, MARQUES.length);
|
assert.equal(new Set(MARQUES.map((m) => m.nom)).size, MARQUES.length);
|
||||||
|
|
@ -91,6 +91,44 @@ describe('registre des marques', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('les trois sortes de conflit (§ 7.5, § 12.7)', () => {
|
||||||
|
test('une par règle localisable, dans l’ordre du moteur : sa règle, la marque de sa trame, le jeton de sa teinte, la forme de son motif', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
CONFLITS.map((c) => [c.regle, c.marque, c.jeton, c.forme]),
|
||||||
|
[
|
||||||
|
['separerAppartenances', 'trameAppartenance', '--couleur-conflit-appartenance', 'hachuresDescendantes'],
|
||||||
|
['nouveauxVoisins', 'trameVoisins', '--couleur-conflit-voisins', 'hachuresMontantes'],
|
||||||
|
['nouvelleTable', 'trameTable', '--couleur-conflit-table', 'points'],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.ok(CONFLITS.every((c) => Object.keys(c).sort().join() === 'forme,jeton,marque,regle'));
|
||||||
|
assert.ok(Object.isFrozen(CONFLITS) && CONFLITS.every((c) => Object.isFrozen(c)));
|
||||||
|
assert.deepEqual(JETONS_CONFLIT, CONFLITS.map(({ jeton }) => jeton));
|
||||||
|
assert.ok(Object.isFrozen(JETONS_CONFLIT));
|
||||||
|
// Les seuils d'écart perçu : entre deux teintes de conflit, et d'une
|
||||||
|
// teinte de conflit à une autre marque du plan.
|
||||||
|
assert.deepEqual(SEPARATIONS, { entreConflits: 12, autresMarques: 5 });
|
||||||
|
assert.ok(Object.isFrozen(SEPARATIONS));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chaque trame est au registre, peinte de son jeton sur le fond du plateau', () => {
|
||||||
|
for (const { marque, jeton } of CONFLITS) {
|
||||||
|
const entree = parNom(MARQUES, marque);
|
||||||
|
assert.notEqual(entree, undefined, marque);
|
||||||
|
assert.equal(entree.jeton, jeton, marque);
|
||||||
|
assert.deepEqual(entree.fonds, ['--couleur-table-fond'], marque);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('teinte réservée au registre : une teinte de conflit ne peint que sa trame, aucune autre marque ni aucun texte', () => {
|
||||||
|
const peintes = [...MARQUES, ...TEXTES].filter((e) => JETONS_CONFLIT.includes(e.jeton) || e.fonds.some((f) => JETONS_CONFLIT.includes(f)));
|
||||||
|
assert.deepEqual(peintes.map((e) => e.nom), CONFLITS.map(({ marque }) => marque));
|
||||||
|
// Le contour épaissi d'une table en conflit est commun aux trois règles :
|
||||||
|
// il ne porte aucune de leurs teintes.
|
||||||
|
assert.ok(!JETONS_CONFLIT.includes(parNom(MARQUES, 'contourConflit').jeton));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('registre des textes', () => {
|
describe('registre des textes', () => {
|
||||||
test('page, secondaire, bandeau, refus, avis, listes du plan, pastille ; seuil 4,5', () => {
|
test('page, secondaire, bandeau, refus, avis, listes du plan, pastille ; seuil 4,5', () => {
|
||||||
assert.deepEqual(TEXTES.map((t) => t.nom), ['page', 'secondaire', 'bandeau', 'refus', 'avis', 'listes', 'pastille']);
|
assert.deepEqual(TEXTES.map((t) => t.nom), ['page', 'secondaire', 'bandeau', 'refus', 'avis', 'listes', 'pastille']);
|
||||||
|
|
@ -129,4 +167,25 @@ describe('palette de jetons.css validée par calcul (§ 12.7, § 14.5)', () => {
|
||||||
test('chaque texte tient 4,5:1 sur chacun de ses fonds, dans les deux thèmes, sous les trois simulations', () => {
|
test('chaque texte tient 4,5:1 sur chacun de ses fonds, dans les deux thèmes, sous les trois simulations', () => {
|
||||||
assert.deepEqual(verifierPalette(JETONS, TEXTES), []);
|
assert.deepEqual(verifierPalette(JETONS, TEXTES), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Les autres marques du plan : celles qui se peignent sur le sol, le
|
||||||
|
// plateau ou une chaise, trames de conflit exceptées, chaque jeton une fois.
|
||||||
|
const FONDS_DU_PLAN = new Set(['--couleur-sol', '--couleur-table-fond', ...REMPLISSAGES_CHAISE]);
|
||||||
|
const autresMarquesDuPlan = () => [
|
||||||
|
...new Set(MARQUES.filter((m) => !JETONS_CONFLIT.includes(m.jeton) && m.fonds.some((f) => FONDS_DU_PLAN.has(f))).map((m) => m.jeton)),
|
||||||
|
];
|
||||||
|
|
||||||
|
test(`les trois teintes de conflit se séparent deux à deux de ${SEPARATIONS.entreConflits} au moins (OKLab × 100), dans les deux thèmes, en couleurs d’origine et sous les trois simulations`, () => {
|
||||||
|
assert.equal(JETONS_CONFLIT.length, 3);
|
||||||
|
assert.deepEqual(verifierSeparation(JETONS, JETONS_CONFLIT, [], SEPARATIONS.entreConflits), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`chaque teinte de conflit se sépare de ${SEPARATIONS.autresMarques} au moins de chaque autre marque du plan, dans les deux thèmes, sous les trois simulations`, () => {
|
||||||
|
const autres = autresMarquesDuPlan();
|
||||||
|
assert.ok(autres.length >= 10, `${autres.length} autres marques`);
|
||||||
|
for (const jeton of ['--couleur-gel', '--couleur-mise-en-evidence', '--couleur-selection', '--couleur-focus', '--couleur-fantome', '--couleur-pastille-fond']) {
|
||||||
|
assert.ok(autres.includes(jeton), jeton);
|
||||||
|
}
|
||||||
|
assert.deepEqual(verifierSeparation(JETONS, JETONS_CONFLIT, autres, SEPARATIONS.autresMarques), []);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
|
|
||||||
// La palette se valide par calcul (§ 12.7, § 14.5). Le module lit les jetons
|
// La palette se valide par calcul (§ 12.7, § 14.5). Le module lit les jetons
|
||||||
// de couleur dans le texte de jetons.css, simule les trois dichromaties,
|
// de couleur dans le texte de jetons.css, simule les trois dichromaties,
|
||||||
// mesure le contraste au sens des WCAG et contrôle la teinte réservée au
|
// mesure le contraste au sens des WCAG et l'écart perçu entre deux
|
||||||
// conflit. Il est pur : il reçoit le texte de la feuille, ne lit aucun
|
// couleurs, et contrôle les teintes réservées aux conflits. Il est pur : il
|
||||||
// fichier et ne touche pas le document.
|
// reçoit le texte de la feuille, ne lit aucun fichier et ne touche pas le
|
||||||
|
// document.
|
||||||
//
|
//
|
||||||
// Une couleur est un tableau [r, g, b, a] : composantes sRGB de 0 à 255,
|
// Une couleur est un tableau [r, g, b, a] : composantes sRGB de 0 à 255,
|
||||||
// alpha de 0 à 1.
|
// alpha de 0 à 1.
|
||||||
|
|
@ -262,30 +263,92 @@ export function teinte(couleur) {
|
||||||
return degres < 0 ? degres + 360 : degres;
|
return degres < 0 ? degres + 360 : degres;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** § 12.7 : la teinte de chaque jeton chromatique se tient à ECART_TEINTE
|
/** § 12.7 : chaque teinte réservée — celles des conflits — est chromatique,
|
||||||
* degrés au moins de celle du conflit, dans chaque thème. jetonsDuConflit :
|
* et la teinte de chaque autre jeton chromatique se tient à ECART_TEINTE
|
||||||
* le jeton de conflit d'abord, puis ceux qui ne peignent que les marques de
|
* degrés au moins de chacune, dans chaque thème. Les teintes réservées ne
|
||||||
* conflit, que le contrôle ne mesure pas. Rend les fautes [{theme, jeton,
|
* se mesurent pas entre elles : verifierSeparation les distingue. Rend les
|
||||||
* ecart}], ecart en degrés sur le cercle ; un jeton de conflit achromatique
|
* fautes [{theme, reservee, jeton, ecart}], ecart en degrés sur le cercle,
|
||||||
* est lui-même une faute, d'écart null — aucune teinte n'est alors réservée. */
|
* dans l'ordre thème, teinte réservée, jeton ; une teinte réservée
|
||||||
export function verifierTeinteReservee(texteCss, jetonsDuConflit) {
|
* achromatique est elle-même une faute, jeton égal à reservee et ecart
|
||||||
const [conflit] = jetonsDuConflit;
|
* null — aucune teinte n'est alors réservée. Un jeton réservé absent d'un
|
||||||
|
* thème lève. */
|
||||||
|
export function verifierTeintesReservees(texteCss, reservees) {
|
||||||
const fautes = [];
|
const fautes = [];
|
||||||
for (const theme of THEMES) {
|
for (const theme of THEMES) {
|
||||||
const jetons = lireJetons(texteCss, theme);
|
const jetons = lireJetons(texteCss, theme);
|
||||||
if (!jetons.has(conflit)) throw new Error(`jeton absent du thème ${theme} : ${conflit}`);
|
for (const reservee of reservees) {
|
||||||
const reference = teinte(jetons.get(conflit));
|
if (!jetons.has(reservee)) throw new Error(`jeton absent du thème ${theme} : ${reservee}`);
|
||||||
if (reference === null) {
|
const reference = teinte(jetons.get(reservee));
|
||||||
fautes.push({ theme, jeton: conflit, ecart: null });
|
if (reference === null) {
|
||||||
continue;
|
fautes.push({ theme, reservee, jeton: reservee, ecart: null });
|
||||||
}
|
continue;
|
||||||
for (const [nom, couleur] of jetons) {
|
}
|
||||||
if (jetonsDuConflit.includes(nom)) continue;
|
for (const [nom, couleur] of jetons) {
|
||||||
const h = teinte(couleur);
|
if (reservees.includes(nom)) continue;
|
||||||
if (h === null) continue;
|
const h = teinte(couleur);
|
||||||
const brut = Math.abs(h - reference);
|
if (h === null) continue;
|
||||||
const ecart = Math.min(brut, 360 - brut);
|
const brut = Math.abs(h - reference);
|
||||||
if (ecart < ECART_TEINTE) fautes.push({ theme, jeton: nom, ecart });
|
const ecart = Math.min(brut, 360 - brut);
|
||||||
|
if (ecart < ECART_TEINTE) fautes.push({ theme, reservee, jeton: nom, ecart });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fautes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Les coordonnées OKLab [L, a, b] d'une couleur sRGB (Ottosson) : L vaut 0
|
||||||
|
* pour le noir et 1 pour le blanc, a et b sont nuls pour un gris. Les
|
||||||
|
* composantes passent en RGB linéaire, puis dans l'espace LMS, dont la
|
||||||
|
* racine cubique se recompose en L, a, b. Alpha ignoré. */
|
||||||
|
export function oklab(couleur) {
|
||||||
|
const [r, g, b] = couleur.slice(0, 3).map(versLineaire);
|
||||||
|
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
||||||
|
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
||||||
|
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
||||||
|
return [
|
||||||
|
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
||||||
|
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
||||||
|
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** L'écart perçu entre deux couleurs : la distance euclidienne de leurs
|
||||||
|
* coordonnées OKLab, multipliée par 100 — 100 du noir au blanc, 0 d'une
|
||||||
|
* couleur à elle-même. Symétrique. Sous une dichromatie, il se mesure sur
|
||||||
|
* les couleurs simulées (§ 12.7 : simuler d'abord, puis mesurer). */
|
||||||
|
export function ecartPercu(a, b) {
|
||||||
|
const [la, aa, ba] = oklab(a);
|
||||||
|
const [lb, ab, bb] = oklab(b);
|
||||||
|
return 100 * Math.hypot(la - lb, aa - ab, ba - bb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Les jetons se distinguent, sur les couleurs d'origine puis sous chaque
|
||||||
|
* simulation, dans les deux thèmes : chaque paire de jetons, puis chaque
|
||||||
|
* jeton contre chacun des autres, tient un écart perçu (ecartPercu) d'au
|
||||||
|
* moins seuil ; les autres ne se mesurent pas entre eux. Rend les fautes
|
||||||
|
* [{theme, simulation, jeton, autre, ecart, seuil}], simulation valant
|
||||||
|
* 'origine' pour les couleurs d'origine, dans l'ordre thème, simulation,
|
||||||
|
* paires de jetons, puis jetons contre autres ; [] quand tout tient. Un
|
||||||
|
* jeton absent d'un thème lève. */
|
||||||
|
export function verifierSeparation(texteCss, jetons, autres, seuil) {
|
||||||
|
const paires = [];
|
||||||
|
jetons.forEach((jeton, i) => {
|
||||||
|
for (const autre of jetons.slice(i + 1)) paires.push([jeton, autre]);
|
||||||
|
});
|
||||||
|
for (const jeton of jetons) for (const autre of autres) paires.push([jeton, autre]);
|
||||||
|
const fautes = [];
|
||||||
|
for (const theme of THEMES) {
|
||||||
|
const lus = lireJetons(texteCss, theme);
|
||||||
|
const lire = (nom) => {
|
||||||
|
if (!lus.has(nom)) throw new Error(`jeton absent du thème ${theme} : ${nom}`);
|
||||||
|
return lus.get(nom);
|
||||||
|
};
|
||||||
|
for (const simulation of ['origine', ...ORDRE_SIMULATIONS]) {
|
||||||
|
const vue = (c) => (simulation === 'origine' ? c : simuler(c, simulation));
|
||||||
|
for (const [jeton, autre] of paires) {
|
||||||
|
const ecart = ecartPercu(vue(lire(jeton)), vue(lire(autre)));
|
||||||
|
if (ecart < seuil) fautes.push({ theme, simulation, jeton, autre, ecart, seuil });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fautes;
|
return fautes;
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,25 @@
|
||||||
// réservée (§ 12.7, § 13.3, § 14.5). Les fonctions s'éprouvent d'abord sur des
|
// réservée (§ 12.7, § 13.3, § 14.5). Les fonctions s'éprouvent d'abord sur des
|
||||||
// feuilles d'épreuve écrites ici, où chaque défaut est posé exprès, puis sur
|
// feuilles d'épreuve écrites ici, où chaque défaut est posé exprès, puis sur
|
||||||
// le vrai fichier de jetons : ses familles, ses deux thèmes, les jetons que
|
// le vrai fichier de jetons : ses familles, ses deux thèmes, les jetons que
|
||||||
// lisent les composants, la teinte du conflit.
|
// lisent les composants, les teintes des trois sortes de conflit.
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readdirSync, readFileSync } from 'node:fs';
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { describe, test } from '../../test/lanceur.js';
|
import { describe, test } from '../../test/lanceur.js';
|
||||||
|
import { CONFLITS, JETONS_CONFLIT } from './marques.js';
|
||||||
import {
|
import {
|
||||||
SIMULATIONS,
|
SIMULATIONS,
|
||||||
contraste,
|
contraste,
|
||||||
|
ecartPercu,
|
||||||
lireBlocs,
|
lireBlocs,
|
||||||
lireJetons,
|
lireJetons,
|
||||||
|
oklab,
|
||||||
simuler,
|
simuler,
|
||||||
teinte,
|
teinte,
|
||||||
verifierPalette,
|
verifierPalette,
|
||||||
verifierTeinteReservee,
|
verifierSeparation,
|
||||||
|
verifierTeintesReservees,
|
||||||
} from './palette.js';
|
} from './palette.js';
|
||||||
|
|
||||||
const DOSSIER = fileURLToPath(new URL('.', import.meta.url));
|
const DOSSIER = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
@ -228,7 +232,7 @@ describe('verifierPalette', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('teinte et verifierTeinteReservee (§ 12.7)', () => {
|
describe('teinte et verifierTeintesReservees (§ 12.7)', () => {
|
||||||
test('la teinte HSL ne vaut que pour une couleur dont les composantes s’écartent de 26 au moins', () => {
|
test('la teinte HSL ne vaut que pour une couleur dont les composantes s’écartent de 26 au moins', () => {
|
||||||
assert.equal(teinte([255, 0, 0, 1]), 0);
|
assert.equal(teinte([255, 0, 0, 1]), 0);
|
||||||
assert.equal(teinte([0, 255, 0, 1]), 120);
|
assert.equal(teinte([0, 255, 0, 1]), 120);
|
||||||
|
|
@ -239,28 +243,47 @@ describe('teinte et verifierTeinteReservee (§ 12.7)', () => {
|
||||||
assert.notEqual(teinte([126, 100, 100, 1]), null); // écart 26 : chromatique
|
assert.notEqual(teinte([126, 100, 100, 1]), null); // écart 26 : chromatique
|
||||||
});
|
});
|
||||||
|
|
||||||
test('un refus de la teinte du conflit et d’une autre clarté est refusé, valeurs pourtant distinctes', () => {
|
test('un refus de la teinte d’un conflit et d’une autre clarté est refusé, valeurs pourtant distinctes', () => {
|
||||||
const conflit = '#c62828';
|
const conflit = '#c62828';
|
||||||
const refus = '#5a1212';
|
const refus = '#5a1212';
|
||||||
assert.notEqual(conflit, refus);
|
assert.notEqual(conflit, refus);
|
||||||
assert.ok(Math.abs(teinte(hex(conflit)) - teinte(hex(refus))) < 1);
|
assert.ok(Math.abs(teinte(hex(conflit)) - teinte(hex(refus))) < 1);
|
||||||
const css = feuille({
|
const css = feuille({
|
||||||
'--couleur-conflit': conflit,
|
'--couleur-conflit-a': conflit,
|
||||||
'--couleur-conflit-trame': '#e57373',
|
'--couleur-conflit-b': '#1f8a3a',
|
||||||
'--couleur-refus': refus,
|
'--couleur-refus': refus,
|
||||||
'--couleur-gris-rose': '#8a7d7d',
|
'--couleur-gris-rose': '#8a7d7d',
|
||||||
'--couleur-bleu': '#1f3a5f',
|
'--couleur-bleu': '#1f3a5f',
|
||||||
});
|
});
|
||||||
const fautes = verifierTeinteReservee(css, ['--couleur-conflit', '--couleur-conflit-trame']);
|
const fautes = verifierTeintesReservees(css, ['--couleur-conflit-a', '--couleur-conflit-b']);
|
||||||
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.jeton}`), ['clair:--couleur-refus', 'sombre:--couleur-refus']);
|
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.reservee}:${f.jeton}`), [
|
||||||
|
'clair:--couleur-conflit-a:--couleur-refus',
|
||||||
|
'sombre:--couleur-conflit-a:--couleur-refus',
|
||||||
|
]);
|
||||||
assert.ok(fautes.every((f) => f.ecart < 30));
|
assert.ok(fautes.every((f) => f.ecart < 30));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('le jeton de conflit lui-même doit être chromatique ; une teinte à 30° passe, à 29° non ; le cercle se referme', () => {
|
test('chaque teinte réservée est une référence : un jeton proche de la seconde est refusé ; deux teintes réservées ne se mesurent pas entre elles', () => {
|
||||||
|
// Rouge à 0°, vert à 120° : le jeton à 135° n'est proche que du vert.
|
||||||
|
const css = feuille({
|
||||||
|
'--couleur-conflit-a': '#ff0000',
|
||||||
|
'--couleur-conflit-b': '#00ff00',
|
||||||
|
'--couleur-conflit-c': '#ff1000',
|
||||||
|
'--couleur-sarcelle': '#00ff40',
|
||||||
|
'--couleur-bleu': '#0000ff',
|
||||||
|
});
|
||||||
|
const fautes = verifierTeintesReservees(css, ['--couleur-conflit-a', '--couleur-conflit-b', '--couleur-conflit-c']);
|
||||||
|
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.reservee}:${f.jeton}`), [
|
||||||
|
'clair:--couleur-conflit-b:--couleur-sarcelle',
|
||||||
|
'sombre:--couleur-conflit-b:--couleur-sarcelle',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une teinte réservée doit être chromatique ; une teinte à 30° passe, à 29° non ; le cercle se referme', () => {
|
||||||
const gris = feuille({ '--couleur-conflit': '#808080', '--couleur-bleu': '#1f3a5f' });
|
const gris = feuille({ '--couleur-conflit': '#808080', '--couleur-bleu': '#1f3a5f' });
|
||||||
assert.deepEqual(verifierTeinteReservee(gris, ['--couleur-conflit']), [
|
assert.deepEqual(verifierTeintesReservees(gris, ['--couleur-conflit']), [
|
||||||
{ theme: 'clair', jeton: '--couleur-conflit', ecart: null },
|
{ theme: 'clair', reservee: '--couleur-conflit', jeton: '--couleur-conflit', ecart: null },
|
||||||
{ theme: 'sombre', jeton: '--couleur-conflit', ecart: null },
|
{ theme: 'sombre', reservee: '--couleur-conflit', jeton: '--couleur-conflit', ecart: null },
|
||||||
]);
|
]);
|
||||||
// Conflit à 0° ; 30° et 330° passent, 29° et 331° non.
|
// Conflit à 0° ; 30° et 330° passent, 29° et 331° non.
|
||||||
const css = feuille({
|
const css = feuille({
|
||||||
|
|
@ -270,13 +293,65 @@ describe('teinte et verifierTeinteReservee (§ 12.7)', () => {
|
||||||
'--couleur-c': '#ff0080', // 329,9°
|
'--couleur-c': '#ff0080', // 329,9°
|
||||||
'--couleur-d': '#ff0079', // 331,5°
|
'--couleur-d': '#ff0079', // 331,5°
|
||||||
});
|
});
|
||||||
const fautes = verifierTeinteReservee(css, ['--couleur-conflit']);
|
const fautes = verifierTeintesReservees(css, ['--couleur-conflit']);
|
||||||
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.jeton}`), [
|
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.jeton}`), [
|
||||||
'clair:--couleur-b', 'clair:--couleur-d', 'sombre:--couleur-b', 'sombre:--couleur-d',
|
'clair:--couleur-b', 'clair:--couleur-d', 'sombre:--couleur-b', 'sombre:--couleur-d',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ecartPercu et verifierSeparation (§ 12.7 : simuler d’abord, puis mesurer l’écart)', () => {
|
||||||
|
test('OKLab : le blanc vaut L = 1, le noir L = 0, un gris n’a ni a ni b ; l’écart du noir au blanc vaut 100, d’une couleur à elle-même 0, dans les deux sens', () => {
|
||||||
|
const blanc = oklab([255, 255, 255, 1]);
|
||||||
|
const noir = oklab([0, 0, 0, 1]);
|
||||||
|
assert.ok(Math.abs(blanc[0] - 1) < 1e-4 && Math.abs(noir[0]) < 1e-9);
|
||||||
|
const gris = oklab([118, 118, 118, 1]);
|
||||||
|
assert.ok(Math.abs(gris[1]) < 1e-4 && Math.abs(gris[2]) < 1e-4, `${gris}`);
|
||||||
|
assert.ok(Math.abs(ecartPercu([0, 0, 0, 1], [255, 255, 255, 1]) - 100) < 0.01);
|
||||||
|
assert.equal(ecartPercu([90, 120, 30, 1], [90, 120, 30, 1]), 0);
|
||||||
|
assert.equal(ecartPercu([200, 30, 30, 1], [30, 120, 200, 1]), ecartPercu([30, 120, 200, 1], [200, 30, 30, 1]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('un rouge et un vert de même clarté se séparent en couleurs d’origine et se confondent sous la deutéranopie : la faute n’est relevée que simulée', () => {
|
||||||
|
// Cherche un vert de la clarté d'un rouge donné : leurs écarts d'origine
|
||||||
|
// sont grands, et la deutéranopie les rapproche.
|
||||||
|
const rouge = [200, 40, 40, 1];
|
||||||
|
let vert = null;
|
||||||
|
for (let v = 40; v <= 255 && vert === null; v += 1) {
|
||||||
|
const candidat = [40, v, 40, 1];
|
||||||
|
if (Math.abs(oklab(candidat)[0] - oklab(rouge)[0]) < 0.01) vert = candidat;
|
||||||
|
}
|
||||||
|
assert.notEqual(vert, null);
|
||||||
|
assert.ok(ecartPercu(rouge, vert) > 20);
|
||||||
|
assert.ok(ecartPercu(simuler(rouge, 'deuteranopie'), simuler(vert, 'deuteranopie')) < 10);
|
||||||
|
const css = feuille({ '--couleur-rouge': enHex(rouge.slice(0, 3)), '--couleur-vert': enHex(vert.slice(0, 3)) });
|
||||||
|
const fautes = verifierSeparation(css, ['--couleur-rouge', '--couleur-vert'], [], 10);
|
||||||
|
assert.ok(fautes.length > 0);
|
||||||
|
assert.ok(fautes.every((f) => f.simulation !== 'origine' && f.ecart < 10 && f.seuil === 10), JSON.stringify(fautes));
|
||||||
|
assert.ok(fautes.some((f) => f.simulation === 'deuteranopie'));
|
||||||
|
assert.ok(fautes.every((f) => f.jeton === '--couleur-rouge' && f.autre === '--couleur-vert'));
|
||||||
|
assert.deepEqual([...new Set(fautes.map((f) => f.theme))], ['clair', 'sombre']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chaque paire des jetons, puis chaque jeton contre chacun des autres ; les autres ne se mesurent pas entre eux ; un jeton absent lève', () => {
|
||||||
|
const css = feuille({
|
||||||
|
'--couleur-a': '#c01010',
|
||||||
|
'--couleur-b': '#c11111',
|
||||||
|
'--couleur-x': '#1010c0',
|
||||||
|
'--couleur-y': '#1111c1',
|
||||||
|
});
|
||||||
|
const fautes = verifierSeparation(css, ['--couleur-a', '--couleur-x'], ['--couleur-b', '--couleur-y'], 5);
|
||||||
|
// Origine et trois simulations, deux thèmes : a contre b et x contre y
|
||||||
|
// se confondent partout ; a contre x, a contre y, x contre b se séparent.
|
||||||
|
assert.deepEqual(
|
||||||
|
[...new Set(fautes.map((f) => `${f.jeton}/${f.autre}`))],
|
||||||
|
['--couleur-a/--couleur-b', '--couleur-x/--couleur-y'],
|
||||||
|
);
|
||||||
|
assert.equal(fautes.length, 2 * 2 * 4);
|
||||||
|
assert.throws(() => verifierSeparation(css, ['--couleur-a'], ['--couleur-absente'], 5), /--couleur-absente/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Fichiers .svelte et .css de src/interface, sous-dossiers compris, triés.
|
// Fichiers .svelte et .css de src/interface, sous-dossiers compris, triés.
|
||||||
function sourcesDeStyle() {
|
function sourcesDeStyle() {
|
||||||
return readdirSync(DOSSIER, { recursive: true, withFileTypes: true })
|
return readdirSync(DOSSIER, { recursive: true, withFileTypes: true })
|
||||||
|
|
@ -342,15 +417,21 @@ describe('jetons.css', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('la teinte du conflit est réservée (§ 12.7) : chromatique, sa valeur à lui seul, à 30° de tout autre jeton chromatique', () => {
|
test('les trois teintes de conflit sont réservées (§ 12.7) : chromatiques, chacune sa valeur, à 30° de tout autre jeton chromatique', () => {
|
||||||
|
assert.deepEqual(JETONS_CONFLIT, ['--couleur-conflit-appartenance', '--couleur-conflit-voisins', '--couleur-conflit-table']);
|
||||||
for (const theme of ['clair', 'sombre']) {
|
for (const theme of ['clair', 'sombre']) {
|
||||||
const jetons = lireJetons(JETONS, theme);
|
const jetons = lireJetons(JETONS, theme);
|
||||||
const conflit = jetons.get('--couleur-conflit');
|
for (const nom of JETONS_CONFLIT) {
|
||||||
assert.notEqual(conflit, undefined, theme);
|
const couleur = jetons.get(nom);
|
||||||
assert.notEqual(teinte(conflit), null, `${theme} : conflit achromatique`);
|
assert.notEqual(couleur, undefined, `${theme} : ${nom}`);
|
||||||
const memeValeur = [...jetons].filter(([n, c]) => n !== '--couleur-conflit' && String(c) === String(conflit));
|
assert.notEqual(teinte(couleur), null, `${theme} : ${nom} achromatique`);
|
||||||
assert.deepEqual(memeValeur.map(([n]) => n), [], theme);
|
const memeValeur = [...jetons].filter(([n, c]) => n !== nom && String(c) === String(couleur));
|
||||||
|
assert.deepEqual(memeValeur.map(([n]) => n), [], `${theme} : ${nom}`);
|
||||||
|
}
|
||||||
|
// Aucun autre jeton ne porte le préfixe des teintes de conflit.
|
||||||
|
assert.deepEqual([...jetons.keys()].filter((n) => n.startsWith('--couleur-conflit')), JETONS_CONFLIT, theme);
|
||||||
}
|
}
|
||||||
assert.deepEqual(verifierTeinteReservee(JETONS, ['--couleur-conflit']), []);
|
assert.deepEqual(verifierTeintesReservees(JETONS, JETONS_CONFLIT), []);
|
||||||
|
assert.deepEqual(CONFLITS.map(({ jeton }) => jeton), JETONS_CONFLIT);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
|
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// La barre de vue du plan (§ 7.2, § 7.7, § 8.4) : le sélecteur de tour,
|
// La barre de vue du plan (§ 7.2, § 7.5, § 7.7, § 8.4) : le sélecteur de
|
||||||
// « Zoom + », « Zoom moins », « Ajuster », l'affichage des listes et le plein
|
// tour, « Zoom + », « Zoom moins », « Ajuster », l'affichage des listes, le
|
||||||
// écran. Des contrôles HTML, hors du <svg data-plan>, tous vivants en
|
// plein écran et la légende des conflits. Des contrôles HTML, hors du <svg data-plan>, tous vivants en
|
||||||
// lecture comme sur un plan bloqué : aucun n'écrit l'événement. Le tour,
|
// lecture comme sur un plan bloqué : aucun n'écrit l'événement. Le tour,
|
||||||
// les listes et le plein écran passent par l'état de navigation ('ui') ;
|
// les listes et le plein écran passent par l'état de navigation ('ui') ;
|
||||||
// les zooms et l'ajustement par Plan.svelte, qui tient la vue :
|
// les zooms et l'ajustement par Plan.svelte, qui tient la vue :
|
||||||
|
|
@ -17,6 +17,12 @@
|
||||||
// ou non ; le menu de la personne la pose (MenuPersonne). Une personne que
|
// ou non ; le menu de la personne la pose (MenuPersonne). Une personne que
|
||||||
// la charge n'a plus se dit telle, sans nom.
|
// la charge n'a plus se dit telle, sans nom.
|
||||||
//
|
//
|
||||||
|
// legende : vrai quand la légende des conflits est ouverte ; basculerLegende
|
||||||
|
// l'ouvre ou la ferme ; idLegende, l'identifiant de la légende, que le
|
||||||
|
// bouton désigne par aria-controls. Plan.svelte tient cet état : ouvrir la
|
||||||
|
// légende n'écrit rien. Le plein écran reste le dernier contrôle de la
|
||||||
|
// barre, celui qui précède le plan dans l'ordre de tabulation.
|
||||||
|
//
|
||||||
// source : ce que la barre dit du placement montré, que rend sourceMontree
|
// source : ce que la barre dit du placement montré, que rend sourceMontree
|
||||||
// (vue_plan.js) — {libelle, revenir} — : la barre le nomme, [data-source],
|
// (vue_plan.js) — {libelle, revenir} — : la barre le nomme, [data-source],
|
||||||
// et, quand une proposition est demandée, offre de ne plus la montrer
|
// et, quand une proposition est demandée, offre de ne plus la montrer
|
||||||
|
|
@ -25,7 +31,7 @@
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { toursOfferts } from './vue_plan.js';
|
import { toursOfferts } from './vue_plan.js';
|
||||||
|
|
||||||
let { tour, tours, zoomer, ajuster, evidence = null, source } = $props();
|
let { tour, tours, zoomer, ajuster, evidence = null, source, legende = false, basculerLegende = () => {}, idLegende = undefined } = $props();
|
||||||
|
|
||||||
const t = getContext('t');
|
const t = getContext('t');
|
||||||
const ui = getContext('ui');
|
const ui = getContext('ui');
|
||||||
|
|
@ -54,6 +60,9 @@
|
||||||
<button type="button" data-commande="listes" aria-pressed={$ui.listes} onclick={() => ui.basculerListes()}
|
<button type="button" data-commande="listes" aria-pressed={$ui.listes} onclick={() => ui.basculerListes()}
|
||||||
>{t('plan.vue.listes')}</button
|
>{t('plan.vue.listes')}</button
|
||||||
>
|
>
|
||||||
|
<button type="button" data-commande="legende" aria-pressed={legende} aria-controls={idLegende} onclick={() => basculerLegende()}
|
||||||
|
>{t('plan.legende.ouvrir')}</button
|
||||||
|
>
|
||||||
<button type="button" data-commande="pleinEcran" aria-pressed={$ui.pleinEcran} onclick={() => ui.basculerPleinEcran()}
|
<button type="button" data-commande="pleinEcran" aria-pressed={$ui.pleinEcran} onclick={() => ui.basculerPleinEcran()}
|
||||||
>{t('plan.vue.pleinEcran')}</button
|
>{t('plan.vue.pleinEcran')}</button
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,15 @@ import { rechercher } from '../../moteur/recherche.js';
|
||||||
import { analyser, configurationDepuisCharge, creerCharge } from '../../stockage/document.js';
|
import { analyser, configurationDepuisCharge, creerCharge } from '../../stockage/document.js';
|
||||||
import { versFichier } from '../../stockage/placements.js';
|
import { versFichier } from '../../stockage/placements.js';
|
||||||
import { fixerCadre } from '../../../test/navigateur/entrees.js';
|
import { fixerCadre } from '../../../test/navigateur/entrees.js';
|
||||||
import { composantes, couleursPeintes, fondPeintSous, sousTheme, visible } from '../../../test/navigateur/rendu.js';
|
import { composantes, contraste, couleurTexte, couleursPeintes, fondPeintSous, sousTheme, visible } from '../../../test/navigateur/rendu.js';
|
||||||
import TEXTE_TOUTES_EN_CONFLIT from '../../../test/fixtures/plan/toutes_en_conflit.gtt.json?raw';
|
import TEXTE_TOUTES_EN_CONFLIT from '../../../test/fixtures/plan/toutes_en_conflit.gtt.json?raw';
|
||||||
import { TABLES_INTERFACE } from '../libelles.js';
|
import { TABLES_INTERFACE } from '../libelles.js';
|
||||||
|
import { CONFLITS } from '../marques.js';
|
||||||
import BandeauPlan from './BandeauPlan.svelte';
|
import BandeauPlan from './BandeauPlan.svelte';
|
||||||
|
import Legende from './Legende.svelte';
|
||||||
import EchellePlan from './EchellePlan.svelte';
|
import EchellePlan from './EchellePlan.svelte';
|
||||||
import { creerMesureur } from './mesure_texte.js';
|
import { creerMesureur } from './mesure_texte.js';
|
||||||
import { LARGEUR_ECHELLE_PX, modeleDessin } from './modele.js';
|
import { LARGEUR_ECHELLE_PX, legendeConflits, modeleDessin } from './modele.js';
|
||||||
import PlanDessin from './PlanDessin.svelte';
|
import PlanDessin from './PlanDessin.svelte';
|
||||||
|
|
||||||
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
||||||
|
|
@ -227,6 +229,8 @@ function rencontreRepetee() {
|
||||||
// --- Lectures --------------------------------------------------------------------
|
// --- Lectures --------------------------------------------------------------------
|
||||||
|
|
||||||
const tous = (racine, selecteur) => [...racine.querySelectorAll(selecteur)];
|
const tous = (racine, selecteur) => [...racine.querySelectorAll(selecteur)];
|
||||||
|
// Les trames de conflit, des trois sortes (CONFLITS).
|
||||||
|
const TRAMES = CONFLITS.map(({ marque }) => `[data-marque=${marque}]`).join(', ');
|
||||||
const plateauDe = (svg, id) => svg.querySelector(`[data-table="${id}"] :is([data-marque=contourTable], [data-marque=contourConflit])`);
|
const plateauDe = (svg, id) => svg.querySelector(`[data-table="${id}"] :is([data-marque=contourTable], [data-marque=contourConflit])`);
|
||||||
const centre = (element) => {
|
const centre = (element) => {
|
||||||
const b = element.getBoundingClientRect();
|
const b = element.getBoundingClientRect();
|
||||||
|
|
@ -610,11 +614,13 @@ describe('le signal de conflit (§ 7.5, § 13.3)', () => {
|
||||||
expect(enConflit.dataset.marque).toBe('contourConflit');
|
expect(enConflit.dataset.marque).toBe('contourConflit');
|
||||||
expect(parseFloat(getComputedStyle(enConflit).strokeWidth)).toBeGreaterThan(parseFloat(getComputedStyle(saine).strokeWidth));
|
expect(parseFloat(getComputedStyle(enConflit).strokeWidth)).toBeGreaterThan(parseFloat(getComputedStyle(saine).strokeWidth));
|
||||||
expect(couleursPeintes(enConflit).stroke).not.toEqual(couleursPeintes(saine).stroke);
|
expect(couleursPeintes(enConflit).stroke).not.toEqual(couleursPeintes(saine).stroke);
|
||||||
const trames = tous(svg, '[data-marque=trameConflit]');
|
// La seule règle active, nouveaux voisins : sa trame seule.
|
||||||
|
const trames = tous(svg, TRAMES);
|
||||||
expect(trames.length).toBe(1);
|
expect(trames.length).toBe(1);
|
||||||
|
expect(trames[0].dataset.marque).toBe('trameVoisins');
|
||||||
expect(trames[0].closest('[data-table]').dataset.table).toBe('1');
|
expect(trames[0].closest('[data-table]').dataset.table).toBe('1');
|
||||||
expect(getComputedStyle(trames[0]).fill).toMatch(/^url\("?#/);
|
expect(getComputedStyle(trames[0]).fill).toMatch(/^url\("?#/);
|
||||||
expect(couleursPeintes(trames[0]).fill.some((c) => memeCouleur(c, jeton('--couleur-conflit')))).toBe(true);
|
expect(couleursPeintes(trames[0]).fill.some((c) => memeCouleur(c, jeton('--couleur-conflit-voisins')))).toBe(true);
|
||||||
const pastilles = tous(svg, '[data-pastille]');
|
const pastilles = tous(svg, '[data-pastille]');
|
||||||
expect(pastilles.length).toBe(1);
|
expect(pastilles.length).toBe(1);
|
||||||
expect(pastilles[0].dataset.pastille).toBe('1');
|
expect(pastilles[0].dataset.pastille).toBe('1');
|
||||||
|
|
@ -631,7 +637,7 @@ describe('le signal de conflit (§ 7.5, § 13.3)', () => {
|
||||||
|
|
||||||
const tour1 = monterPlan(modeleDe(charge, { tour: 1, proposition: 1, k: 1.5 }), vue).svg;
|
const tour1 = monterPlan(modeleDe(charge, { tour: 1, proposition: 1, k: 1.5 }), vue).svg;
|
||||||
expect(plateauDe(tour1, 1).dataset.marque).toBe('contourTable');
|
expect(plateauDe(tour1, 1).dataset.marque).toBe('contourTable');
|
||||||
expect(tous(tour1, '[data-marque=trameConflit], [data-pastille]').length).toBe(0);
|
expect(tous(tour1, `${TRAMES}, [data-pastille]`).length).toBe(0);
|
||||||
expect(getComputedStyle(plateauDe(tour1, 1)).strokeWidth).toBe(getComputedStyle(plateauDe(tour1, 2)).strokeWidth);
|
expect(getComputedStyle(plateauDe(tour1, 1)).strokeWidth).toBe(getComputedStyle(plateauDe(tour1, 2)).strokeWidth);
|
||||||
|
|
||||||
const reserve = chargeDeTables([8]);
|
const reserve = chargeDeTables([8]);
|
||||||
|
|
@ -648,7 +654,7 @@ describe('le signal de conflit (§ 7.5, § 13.3)', () => {
|
||||||
const modele = modeleDe(charge, { proposition: 1, k: 1 });
|
const modele = modeleDe(charge, { proposition: 1, k: 1 });
|
||||||
expect(modele.signalerConflits).toBe(false);
|
expect(modele.signalerConflits).toBe(false);
|
||||||
const { svg } = monterPlan(modele, { k: 1, tx: 150, ty: 250 });
|
const { svg } = monterPlan(modele, { k: 1, tx: 150, ty: 250 });
|
||||||
expect(tous(svg, '[data-marque=contourConflit], [data-marque=trameConflit]').length).toBe(0);
|
expect(tous(svg, `[data-marque=contourConflit], ${TRAMES}`).length).toBe(0);
|
||||||
const saine = monterPlan(modeleDe(chargeDeTables([3]), { k: 1 }), { k: 1, tx: 150, ty: 600 }).svg;
|
const saine = monterPlan(modeleDe(chargeDeTables([3]), { k: 1 }), { k: 1, tx: 150, ty: 600 }).svg;
|
||||||
const reference = getComputedStyle(plateauDe(saine, 1));
|
const reference = getComputedStyle(plateauDe(saine, 1));
|
||||||
for (const id of [1, 2, 3, 4]) {
|
for (const id of [1, 2, 3, 4]) {
|
||||||
|
|
@ -682,6 +688,139 @@ describe('le signal de conflit (§ 7.5, § 13.3)', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Les trois sortes de conflit (§ 7.5, § 7.6, § 12.7) ---------------------------
|
||||||
|
|
||||||
|
// Le motif d'une trame : le <pattern> que désigne son remplissage.
|
||||||
|
function motifDe(trame) {
|
||||||
|
const id = /^url\("?#([^")]+)"?\)/.exec(getComputedStyle(trame).fill)?.[1];
|
||||||
|
return id === undefined ? null : document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// La forme d'un motif, lue dans le document : 'points' pour des disques ;
|
||||||
|
// pour des traits, leur sens à l'écran — la direction d'un trait du carreau
|
||||||
|
// passée par patternTransform —, 'hachuresDescendantes' quand il descend
|
||||||
|
// vers la droite, 'hachuresMontantes' quand il monte.
|
||||||
|
function formeDe(motif) {
|
||||||
|
const formes = [...motif.children].filter((n) => n instanceof SVGGeometryElement);
|
||||||
|
if (formes.length > 0 && formes.every((n) => n.localName === 'circle')) return 'points';
|
||||||
|
if (formes.length === 0 || !formes.every((n) => n.localName === 'line')) return `formes ${formes.map((n) => n.localName)}`;
|
||||||
|
const [trait] = formes;
|
||||||
|
const dx = trait.x2.baseVal.value - trait.x1.baseVal.value;
|
||||||
|
const dy = trait.y2.baseVal.value - trait.y1.baseVal.value;
|
||||||
|
const m = motif.patternTransform.baseVal.consolidate()?.matrix ?? new DOMMatrix();
|
||||||
|
const [ex, ey] = [m.a * dx + m.c * dy, m.b * dx + m.d * dy];
|
||||||
|
if (Math.abs(ex) < 1e-6 || Math.abs(ey) < 1e-6) return 'traits droits';
|
||||||
|
return ex * ey > 0 ? 'hachuresDescendantes' : 'hachuresMontantes';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les couleurs d'une trame, en texte trié, pour une comparaison d'ensembles.
|
||||||
|
const couleursEnTexte = (couleurs) => couleurs.map((c) => c.slice(0, 3).join(',')).sort();
|
||||||
|
|
||||||
|
// La petite démonstration en conflit, une proposition de rechercher, graine
|
||||||
|
// 3 : son tour 3 signale les trois sortes, une table saine, une table qui
|
||||||
|
// viole les trois règles et une qui en viole deux.
|
||||||
|
function troisSortes() {
|
||||||
|
const charge = analyser(texteLivre('petite-conflit')).charge;
|
||||||
|
const configuration = configurationDepuisCharge(charge);
|
||||||
|
const [proposition] = rechercher(configuration, { graine: 3, nombre: 1, arret: 300, historique: 1000 });
|
||||||
|
return avecProposition(charge, versFichier(proposition, configuration, { produitVersion: 'epreuve', attribuerSieges: false, decalage: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('les trois sortes de conflit : une forme et une teinte par règle (§ 7.5, § 7.6, § 12.7)', () => {
|
||||||
|
for (const theme of ['clair', 'sombre']) {
|
||||||
|
test(`thème ${theme} : chaque règle dessine sa forme dans sa teinte, sur le fond du plateau ; une table qui en viole plusieurs les superpose, dans l\u{2019}ordre du moteur`, async () => {
|
||||||
|
await sousTheme(theme, async () => {
|
||||||
|
const charge = troisSortes();
|
||||||
|
const fond = couleursEnTexte([jeton('--couleur-table-fond')]);
|
||||||
|
const formes = new Map();
|
||||||
|
let superposees = 0;
|
||||||
|
for (let tour = 1; tour <= charge.evenement.tours; tour += 1) {
|
||||||
|
const modele = modeleDe(charge, { tour, proposition: 1, k: 1 });
|
||||||
|
if (!modele.signalerConflits) continue;
|
||||||
|
const { svg } = monterPlan(modele, { k: 1, tx: 150, ty: 250 });
|
||||||
|
for (const table of modele.tables) {
|
||||||
|
const trames = tous(svg, `[data-table="${table.id}"] :is(${TRAMES})`);
|
||||||
|
expect(trames.map((e) => e.dataset.marque), `tour ${tour}, table ${table.id}`).toEqual(table.trames.map(({ marque }) => marque));
|
||||||
|
if (trames.length > 1) superposees += 1;
|
||||||
|
for (const [rang, trame] of trames.entries()) {
|
||||||
|
const { regle, jeton: nom, forme } = table.trames[rang];
|
||||||
|
expect(trame.checkVisibility({ opacityProperty: true, visibilityProperty: true })).toBe(true);
|
||||||
|
const motif = motifDe(trame);
|
||||||
|
expect(motif?.localName, regle).toBe('pattern');
|
||||||
|
expect(formeDe(motif), regle).toBe(forme);
|
||||||
|
// Le motif ne peint que sa teinte et le fond du plateau : le
|
||||||
|
// halo de ses traits, qui les détache d'une trame dessous.
|
||||||
|
expect(couleursEnTexte(couleursPeintes(trame).fill), regle).toEqual([...couleursEnTexte([jeton(nom)]), ...fond].sort());
|
||||||
|
formes.set(regle, forme);
|
||||||
|
}
|
||||||
|
// Les trames suivent le plateau, et la pastille reste sur son socle.
|
||||||
|
if (trames.length > 0) {
|
||||||
|
const plateau = plateauDe(svg, table.id);
|
||||||
|
expect(trames.every((e) => plateau.compareDocumentPosition(e) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
||||||
|
const pastille = svg.querySelector(`[data-pastille="${table.id}"] [data-marque=pastille]`);
|
||||||
|
expect(fondPeintSous(pastille)).toEqual([jeton('--couleur-table-fond')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
demonterTout();
|
||||||
|
}
|
||||||
|
expect(Object.fromEntries(formes)).toEqual(Object.fromEntries(CONFLITS.map(({ regle, forme }) => [regle, forme])));
|
||||||
|
expect(new Set(formes.values()).size).toBe(3);
|
||||||
|
expect(superposees).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('la légende des conflits (§ 7.5, § 14.5)', () => {
|
||||||
|
const REGLAGES = { separerAppartenances: true, nouveauxVoisins: true, nouvelleTable: true, varierAppartenances: true, attribuerSieges: false };
|
||||||
|
|
||||||
|
for (const theme of ['clair', 'sombre']) {
|
||||||
|
test(`thème ${theme} : un échantillon par règle active, de la forme et de la teinte du plan, et son nom ; la pastille et ce qu\u{2019}elle compte ; texte à 4,5:1`, async () => {
|
||||||
|
await sousTheme(theme, async () => {
|
||||||
|
const entrees = legendeConflits(REGLAGES);
|
||||||
|
expect(entrees.length).toBe(3);
|
||||||
|
const conteneur = monterHtml(Legende, { entrees });
|
||||||
|
const legende = conteneur.querySelector('[data-legende]');
|
||||||
|
expect(visible(legende.querySelector('h3, h2, p, span'))).toBe(true);
|
||||||
|
const items = tous(legende, '[data-legende-regle]');
|
||||||
|
expect(items.map((e) => e.dataset.legendeRegle)).toEqual(CONFLITS.map(({ regle }) => regle));
|
||||||
|
for (const [rang, item] of items.entries()) {
|
||||||
|
const { marque, jeton: nom, forme, cle, cleForme } = entrees[rang];
|
||||||
|
const trame = item.querySelector(`svg [data-marque="${marque}"]`);
|
||||||
|
expect(trame, marque).not.toBeNull();
|
||||||
|
expect(trame.checkVisibility({ opacityProperty: true, visibilityProperty: true })).toBe(true);
|
||||||
|
expect(formeDe(motifDe(trame))).toBe(forme);
|
||||||
|
expect(couleursEnTexte(couleursPeintes(trame).fill)).toEqual([...couleursEnTexte([jeton(nom)]), ...couleursEnTexte([jeton('--couleur-table-fond')])].sort());
|
||||||
|
expect(item.textContent).toContain(t(cle));
|
||||||
|
expect(item.textContent).toContain(t(cleForme));
|
||||||
|
expect(item.querySelectorAll('svg [data-marque="contourConflit"]').length).toBe(1);
|
||||||
|
}
|
||||||
|
const pastille = legende.querySelector('[data-legende-pastille]');
|
||||||
|
expect(pastille.querySelector('svg [data-marque="pastille"]')).not.toBeNull();
|
||||||
|
expect(pastille.textContent).toContain(t('plan.legende.pastille'));
|
||||||
|
expect(pastille.querySelector('svg text').textContent).toBe(t('plan.legende.pastilleExemple'));
|
||||||
|
// Chaque texte HTML de la légende tient 4,5:1 sur son fond peint.
|
||||||
|
const textes = tous(legende, '*').filter((e) => !(e instanceof SVGElement) && [...e.childNodes].some((n) => n.nodeType === Node.TEXT_NODE && n.textContent.trim() !== ''));
|
||||||
|
expect(textes.length).toBeGreaterThan(3);
|
||||||
|
for (const element of textes) {
|
||||||
|
const [fond] = fondPeintSous(element);
|
||||||
|
expect(contraste(couleurTexte(element), fond), element.textContent).toBeGreaterThanOrEqual(4.5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('une règle inactive n\u{2019}y paraît pas ; aucune règle active : la légende le dit, et garde la pastille', () => {
|
||||||
|
const deux = monterHtml(Legende, { entrees: legendeConflits({ ...REGLAGES, nouveauxVoisins: false }) });
|
||||||
|
expect(tous(deux, '[data-legende-regle]').map((e) => e.dataset.legendeRegle)).toEqual(['separerAppartenances', 'nouvelleTable']);
|
||||||
|
expect(deux.querySelector('[data-marque="trameVoisins"]')).toBeNull();
|
||||||
|
const aucune = monterHtml(Legende, { entrees: legendeConflits({ ...REGLAGES, separerAppartenances: false, nouveauxVoisins: false, nouvelleTable: false }) });
|
||||||
|
expect(tous(aucune, '[data-legende-regle]').length).toBe(0);
|
||||||
|
expect(aucune.querySelector('[data-legende]').textContent).toContain(t('plan.legende.aucune'));
|
||||||
|
expect(aucune.querySelector('[data-legende-pastille]')).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('l\u{2019}échelle graphique et la mention (§ 7.7, § 19.3 07)', () => {
|
describe('l\u{2019}échelle graphique et la mention (§ 7.7, § 19.3 07)', () => {
|
||||||
test('la mention se voit, portée par EchellePlan, tirée des libellés', () => {
|
test('la mention se voit, portée par EchellePlan, tirée des libellés', () => {
|
||||||
const conteneur = monterHtml(EchellePlan, { k: 1 });
|
const conteneur = monterHtml(EchellePlan, { k: 1 });
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@
|
||||||
// trois déclencheurs — le survol, le focus du clavier, la pression longue —
|
// trois déclencheurs — le survol, le focus du clavier, la pression longue —
|
||||||
// et lui passe le détail des conflits (detailConflits, modele.js) et sa
|
// et lui passe le détail des conflits (detailConflits, modele.js) et sa
|
||||||
// position dans la surface, en px. Elle énonce la table et son nombre de
|
// position dans la surface, en px. Elle énonce la table et son nombre de
|
||||||
// violations, puis chaque règle violée et ses groupes : chaque nom occupe
|
// violations, puis chaque ligne du détail et ses groupes : chaque nom
|
||||||
// son propre nœud, relié à l'autre nom d'une paire par « et », séparé du
|
// occupe son propre nœud, relié à l'autre nom d'une paire par « et »,
|
||||||
// groupe suivant.
|
// séparé du groupe suivant. Une ligne qui porte une appartenance — séparer
|
||||||
|
// les appartenances — la nomme dans son propre nœud, entre son libellé et
|
||||||
|
// finAppartenance.
|
||||||
//
|
//
|
||||||
// infobulle : null, ou {detail, x, y} ; id : l'identifiant que la racine
|
// infobulle : null, ou {detail, x, y} ; id : l'identifiant que la racine
|
||||||
// du plan désigne par aria-describedby quand l'infobulle s'ouvre par le
|
// du plan désigne par aria-describedby quand l'infobulle s'ouvre par le
|
||||||
|
|
@ -32,9 +34,11 @@
|
||||||
>
|
>
|
||||||
{#if infobulle !== null}
|
{#if infobulle !== null}
|
||||||
<p class="entete">{t('plan.infobulle.entete', { numero: infobulle.detail.numero, nombre: infobulle.detail.nombre })}</p>
|
<p class="entete">{t('plan.infobulle.entete', { numero: infobulle.detail.numero, nombre: infobulle.detail.nombre })}</p>
|
||||||
{#each infobulle.detail.regles as regle, rang (rang)}
|
{#each infobulle.detail.lignes as ligne, rang (rang)}
|
||||||
<p>
|
<p>
|
||||||
<span>{t(regle.cle)}</span>{#each regle.groupes as groupe, g (g)}{#if g > 0}<span
|
<span>{t(ligne.cle)}</span>{#if ligne.appartenance !== null}<span class="donnee">{ligne.appartenance}</span><span
|
||||||
|
>{t('plan.infobulle.finAppartenance')}</span
|
||||||
|
>{/if}{#each ligne.groupes as groupe, g (g)}{#if g > 0}<span
|
||||||
>{t('plan.infobulle.separateur')}</span
|
>{t('plan.infobulle.separateur')}</span
|
||||||
>{/if}{#each groupe as nom, n (n)}{#if n > 0}<span>{t('plan.infobulle.et')}</span>{/if}<span class="donnee"
|
>{/if}{#each groupe as nom, n (n)}{#if n > 0}<span>{t('plan.infobulle.et')}</span>{/if}<span class="donnee"
|
||||||
>{nom}</span
|
>{nom}</span
|
||||||
|
|
|
||||||
126
src/interface/plan/Legende.svelte
Normal file
126
src/interface/plan/Legende.svelte
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<!-- © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// La légende des conflits du plan (§ 7.5, § 12.7) : pour chaque règle
|
||||||
|
// active, un échantillon dessiné en SVG — un plateau au contour épaissi,
|
||||||
|
// couvert de la trame de la règle, de la forme et de la teinte que le plan
|
||||||
|
// peint, tirée des mêmes motifs (Motifs.svelte) — et son nom, suivi du nom
|
||||||
|
// de sa forme ; puis la pastille et ce qu'elle compte. Une règle inactive
|
||||||
|
// n'y paraît pas ; sans règle active, la légende le dit.
|
||||||
|
//
|
||||||
|
// entrees : celles de legendeConflits (modele.js), dans l'ordre du moteur ;
|
||||||
|
// id : l'identifiant que le bouton de la barre de vue désigne par
|
||||||
|
// aria-controls. Les motifs se définissent dans un <svg> sans taille,
|
||||||
|
// propre à la légende : leurs identifiants portent son préfixe, et les
|
||||||
|
// échantillons les désignent. Les tailles des échantillons sont en px.
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import Motifs from './Motifs.svelte';
|
||||||
|
|
||||||
|
let { entrees, id = undefined } = $props();
|
||||||
|
|
||||||
|
const t = getContext('t');
|
||||||
|
const prefixe = $props.id();
|
||||||
|
|
||||||
|
// Le centre et le rayon du plateau d'un échantillon, et le rayon de la
|
||||||
|
// pastille, en px.
|
||||||
|
const CENTRE = 20;
|
||||||
|
const RAYON = 16;
|
||||||
|
const RAYON_PASTILLE = 12;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="legende" data-legende {id} aria-labelledby="{prefixe}-titre">
|
||||||
|
<svg class="motifs" width="0" height="0" aria-hidden="true"><Motifs {prefixe} /></svg>
|
||||||
|
<h3 id="{prefixe}-titre">{t('plan.legende.titre')}</h3>
|
||||||
|
{#if entrees.length === 0}
|
||||||
|
<p>{t('plan.legende.aucune')}</p>
|
||||||
|
{/if}
|
||||||
|
<ul>
|
||||||
|
{#each entrees as entree (entree.regle)}
|
||||||
|
<li data-legende-regle={entree.regle}>
|
||||||
|
<svg class="echantillon" width={2 * CENTRE} height={2 * CENTRE} aria-hidden="true">
|
||||||
|
<circle class="plateau" data-marque="contourConflit" cx={CENTRE} cy={CENTRE} r={RAYON} />
|
||||||
|
<circle class="trame" data-marque={entree.marque} fill="url(#{prefixe}-trame-{entree.regle})" cx={CENTRE} cy={CENTRE} r={RAYON} />
|
||||||
|
</svg>
|
||||||
|
<span>{t(entree.cle)}</span>
|
||||||
|
<span>{t(entree.cleForme)}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
<li data-legende-pastille>
|
||||||
|
<svg class="echantillon" width={2 * CENTRE} height={2 * CENTRE} aria-hidden="true">
|
||||||
|
<circle class="disque" data-marque="pastille" cx={CENTRE} cy={CENTRE} r={RAYON_PASTILLE} />
|
||||||
|
<text class="nombre" x={CENTRE} y={CENTRE} text-anchor="middle" dominant-baseline="central">{t('plan.legende.pastilleExemple')}</text>
|
||||||
|
</svg>
|
||||||
|
<span>{t('plan.legende.pastille')}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{#if entrees.length > 1}
|
||||||
|
<p>{t('plan.legende.superposition')}</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.legende {
|
||||||
|
padding: var(--espace-2) var(--espace-3);
|
||||||
|
border: 1px solid var(--couleur-page-filet);
|
||||||
|
border-radius: var(--rayon-1);
|
||||||
|
background-color: var(--couleur-page-fond);
|
||||||
|
color: var(--couleur-page-texte);
|
||||||
|
}
|
||||||
|
|
||||||
|
.motifs {
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 var(--espace-2);
|
||||||
|
font-size: var(--taille-texte);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: var(--espace-2) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--espace-2) var(--espace-5);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--espace-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.echantillon {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Le plateau de l'échantillon : le fond et le contour épaissi d'une table
|
||||||
|
en conflit, sous la trame. */
|
||||||
|
.plateau {
|
||||||
|
fill: var(--couleur-table-fond);
|
||||||
|
stroke: var(--couleur-contour-fort);
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trame {
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disque {
|
||||||
|
fill: var(--couleur-pastille-fond);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nombre {
|
||||||
|
fill: var(--couleur-pastille-texte);
|
||||||
|
font-family: var(--police-interface);
|
||||||
|
font-size: var(--taille-texte-petit);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,11 +5,24 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Les motifs du plan (§ 7.5, § 7.6) : la hachure d'une place à pourvoir —
|
// Les motifs du plan (§ 7.5, § 7.6) : la hachure d'une place à pourvoir —
|
||||||
// des traits --couleur-hachure sur le fond de la chaise — et la trame d'une
|
// des traits --couleur-hachure sur le fond de la chaise — et les trois
|
||||||
// table en conflit — des traits --couleur-conflit sur le fond du plateau.
|
// trames de conflit, une par règle (CONFLITS, marques.js), chacune d'une
|
||||||
// Chaque motif peint son fond, opaque, puis ses traits : la couleur relue
|
// forme et d'une teinte : hachures descendantes pour séparer les
|
||||||
// dans le style calculé est la couleur peinte. Les tailles sont en cm de
|
// appartenances, hachures montantes pour nouveaux voisins, points pour
|
||||||
// dessin ; prefixe rend leurs identifiants uniques dans le document.
|
// nouvelle table. L'identifiant d'une trame est <prefixe>-trame-<règle>.
|
||||||
|
//
|
||||||
|
// La hachure peint son fond, opaque, puis ses traits. Une trame de conflit
|
||||||
|
// ne peint pas de fond : elle se pose sur le plateau, et une table qui
|
||||||
|
// viole plusieurs règles superpose ses trames — les hachures des deux sens
|
||||||
|
// se croisent en quadrillage, les points se posent par-dessus. Chaque trait
|
||||||
|
// ou point se peint sur un halo du fond du plateau, plus large que lui, qui
|
||||||
|
// le détache de la trame dessous : une trame ne peint que sa teinte et le
|
||||||
|
// fond du plateau, et sa teinte se mesure contre lui (§ 14.5). La couleur
|
||||||
|
// relue dans le style calculé est la couleur peinte.
|
||||||
|
//
|
||||||
|
// Les tailles sont dans l'unité du dessin qui porte les motifs : des cm sur
|
||||||
|
// le plan, des px dans la légende. prefixe rend les identifiants uniques
|
||||||
|
// dans le document.
|
||||||
//
|
//
|
||||||
// Les cadenas et l'anneau ne sont pas des symboles référencés par <use> :
|
// Les cadenas et l'anneau ne sont pas des symboles référencés par <use> :
|
||||||
// chaque chaise les dessine elle-même (TablePlan), et les aides de rendu
|
// chaque chaise les dessine elle-même (TablePlan), et les aides de rendu
|
||||||
|
|
@ -23,9 +36,26 @@
|
||||||
<rect class="fond-chaise" width="8" height="8" />
|
<rect class="fond-chaise" width="8" height="8" />
|
||||||
<line class="trait-hachure" x1="4" y1="0" x2="4" y2="8" />
|
<line class="trait-hachure" x1="4" y1="0" x2="4" y2="8" />
|
||||||
</pattern>
|
</pattern>
|
||||||
<pattern id="{prefixe}-trame" patternUnits="userSpaceOnUse" width="12" height="12" patternTransform="rotate(-45)">
|
<!-- Séparer les appartenances : un trait du carreau, tourné de −45°, descend vers la droite. -->
|
||||||
<rect class="fond-table" width="12" height="12" />
|
<pattern
|
||||||
<line class="trait-conflit" x1="6" y1="0" x2="6" y2="12" />
|
id="{prefixe}-trame-separerAppartenances"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
patternTransform="rotate(-45)"
|
||||||
|
>
|
||||||
|
<line class="halo" x1="6" y1="0" x2="6" y2="12" />
|
||||||
|
<line class="trait appartenance-conflit" x1="6" y1="0" x2="6" y2="12" />
|
||||||
|
</pattern>
|
||||||
|
<!-- Nouveaux voisins : tourné de 45°, le trait monte vers la droite. -->
|
||||||
|
<pattern id="{prefixe}-trame-nouveauxVoisins" patternUnits="userSpaceOnUse" width="12" height="12" patternTransform="rotate(45)">
|
||||||
|
<line class="halo" x1="6" y1="0" x2="6" y2="12" />
|
||||||
|
<line class="trait voisins-conflit" x1="6" y1="0" x2="6" y2="12" />
|
||||||
|
</pattern>
|
||||||
|
<!-- Nouvelle table : un point par carreau. -->
|
||||||
|
<pattern id="{prefixe}-trame-nouvelleTable" patternUnits="userSpaceOnUse" width="12" height="12">
|
||||||
|
<circle class="halo-point" cx="6" cy="6" r="4.5" />
|
||||||
|
<circle class="point table-conflit" cx="6" cy="6" r="3" />
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
|
|
@ -39,12 +69,30 @@
|
||||||
stroke-width: 3;
|
stroke-width: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fond-table {
|
/* Le halo d'un trait ou d'un point : le fond du plateau, plus large que
|
||||||
|
lui d'un de chaque côté. */
|
||||||
|
.halo {
|
||||||
|
stroke: var(--couleur-table-fond);
|
||||||
|
stroke-width: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.halo-point {
|
||||||
fill: var(--couleur-table-fond);
|
fill: var(--couleur-table-fond);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trait-conflit {
|
.trait {
|
||||||
stroke: var(--couleur-conflit);
|
stroke-width: 4;
|
||||||
stroke-width: 3;
|
}
|
||||||
|
|
||||||
|
.trait.appartenance-conflit {
|
||||||
|
stroke: var(--couleur-conflit-appartenance);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trait.voisins-conflit {
|
||||||
|
stroke: var(--couleur-conflit-voisins);
|
||||||
|
}
|
||||||
|
|
||||||
|
.point.table-conflit {
|
||||||
|
fill: var(--couleur-conflit-table);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,75 @@ describe('le plan est un seul arrêt de tabulation (§ 7.3, § 13.5)', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- L'appartenance nommée, la légende des conflits (§ 7.5) -------------------
|
||||||
|
|
||||||
|
describe('l’infobulle nomme l’appartenance d’une paire réunie ; la légende des conflits (§ 7.5)', () => {
|
||||||
|
const { t } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE]);
|
||||||
|
|
||||||
|
// Deux tables de trois, six personnes, un tour : la table 1 réunit Iris
|
||||||
|
// et Théo, du Club des Merles ; la table 2, Basile et Capucine, d'une
|
||||||
|
// appartenance écrite avec une minuscule et un blanc final, telle que la
|
||||||
|
// liste la porte.
|
||||||
|
function deuxAppartenances(reglages = {}) {
|
||||||
|
const charge = chargeDe({ id: 'evt-appartenances', defaut: 3, places: [null, null], personnes: 6, nombreTours: 1, tours: [[[1, 2, 4], [3, 5, 6]]] });
|
||||||
|
const appartenances = new Map([[1, 'Club des Merles'], [2, 'Club des Merles'], [5, 'cercle des Hérons '], [6, 'cercle des Hérons ']]);
|
||||||
|
for (const personne of charge.participants) personne.appartenance = appartenances.get(personne.id) ?? null;
|
||||||
|
Object.assign(charge.reglages, reglages);
|
||||||
|
return charge;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('le survol d’une table en conflit d’appartenance : « Même appartenance (…) : … et … », l’appartenance dans son propre nœud', async () => {
|
||||||
|
const m = await ouvrir(deuxAppartenances());
|
||||||
|
expect(etatMontre(m).tables.map(({ conflits }) => conflits.nombre)).toEqual([1, 1]);
|
||||||
|
const infobulle = () => document.querySelector('[role="tooltip"]');
|
||||||
|
// Le pointeur part du sol : un survol commence par un mouvement.
|
||||||
|
await pointer('bouger', pointSol(m).x, pointSol(m).y);
|
||||||
|
for (const [table, appartenance, noms] of [
|
||||||
|
[1, 'Club des Merles', ['Iris Ombrelle', 'Théo Pervenche']],
|
||||||
|
[2, 'cercle des Hérons ', ['Basile Tourmaline', 'Capucine Arrosoir']],
|
||||||
|
]) {
|
||||||
|
const point = pointTable(m, table);
|
||||||
|
await pointer('bouger', point.x, point.y);
|
||||||
|
await attendre(() => expect(infobulleVisible(infobulle())).toBe(true));
|
||||||
|
const lignes = [...infobulle().querySelectorAll('p')];
|
||||||
|
expect(lignes.map((p) => p.textContent)).toEqual([
|
||||||
|
t('plan.infobulle.entete', { numero: table, nombre: 1 }),
|
||||||
|
`${t('plan.infobulle.separerAppartenances')}${appartenance}${t('plan.infobulle.finAppartenance')}${noms.join(t('plan.infobulle.et'))}`,
|
||||||
|
]);
|
||||||
|
expect(lignes[1].textContent).toMatch(/^Même appartenance \(/);
|
||||||
|
const noeuds = [...lignes[1].children].map((e) => e.textContent);
|
||||||
|
expect(noeuds).toContain(appartenance);
|
||||||
|
for (const nom of noms) expect(noeuds).toContain(nom);
|
||||||
|
await pointer('bouger', pointSol(m).x, pointSol(m).y);
|
||||||
|
await attendre(() => expect(infobulleVisible(infobulle())).toBe(false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mode of ['ecriture', 'lecture']) {
|
||||||
|
test(`mode ${mode} : le bouton « Légende » de la barre de vue ouvre la légende des seules règles actives, et la referme ; rien ne s’écrit`, async () => {
|
||||||
|
const m = await ouvrir(deuxAppartenances({ nouvelleTable: false }), { mode });
|
||||||
|
const revision = m.seance.etat().evenement.revision;
|
||||||
|
const bouton = dans(m, '[data-commande="legende"]');
|
||||||
|
expect(bouton).not.toBeNull();
|
||||||
|
expect(bouton.closest('[role="toolbar"]')).not.toBeNull();
|
||||||
|
expect(bouton.textContent).toBe(t('plan.legende.ouvrir'));
|
||||||
|
expect(bouton.getAttribute('aria-pressed')).toBe('false');
|
||||||
|
expect(dans(m, '[data-legende]')).toBeNull();
|
||||||
|
await cliquer(bouton);
|
||||||
|
await attendre(() => expect(dans(m, '[data-legende]')).not.toBeNull());
|
||||||
|
expect(bouton.getAttribute('aria-pressed')).toBe('true');
|
||||||
|
expect(bouton.getAttribute('aria-controls')).toBe(dans(m, '[data-legende]').id);
|
||||||
|
expect(tous(m, '[data-legende] [data-legende-regle]').map((e) => e.dataset.legendeRegle)).toEqual(['separerAppartenances', 'nouveauxVoisins']);
|
||||||
|
expect(dans(m, '[data-legende] [data-marque="trameTable"]')).toBeNull();
|
||||||
|
expect(infobulleVisible(dans(m, '[data-legende]'))).toBe(true);
|
||||||
|
await cliquer(bouton);
|
||||||
|
await attendre(() => expect(dans(m, '[data-legende]')).toBeNull());
|
||||||
|
expect(bouton.getAttribute('aria-pressed')).toBe('false');
|
||||||
|
expect(m.seance.etat().evenement.revision).toBe(revision);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- L'infobulle (§ 7.1, § 7.3, § 19.4) ---------------------------------------
|
// --- L'infobulle (§ 7.1, § 7.3, § 19.4) ---------------------------------------
|
||||||
|
|
||||||
describe('l’infobulle : un composant, trois déclencheurs (§ 7.1, § 7.3)', () => {
|
describe('l’infobulle : un composant, trois déclencheurs (§ 7.1, § 7.3)', () => {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
|
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Le plan de salle et sa vue (§ 7.2, § 7.3, § 7.7, § 8.4, § 8.5) : la
|
// Le plan de salle et sa vue (§ 7.2, § 7.3, § 7.5, § 7.7, § 8.4, § 8.5) :
|
||||||
// barre de vue, puis la surface [data-surface] qui porte le bandeau du
|
// la barre de vue, la légende des conflits quand elle est ouverte — les
|
||||||
// plan, le <svg data-plan> et l'échelle graphique. Le <svg> racine n'a ni
|
// règles actives des réglages (legendeConflits) —, puis la surface
|
||||||
|
// [data-surface] qui porte le bandeau du plan, le <svg data-plan> et
|
||||||
|
// l'échelle graphique. Ouvrir la légende change la taille du cadre, que le
|
||||||
|
// ResizeObserver relit comme tout autre changement. Le <svg> racine n'a ni
|
||||||
// viewBox, ni bordure, ni remplissage : son unité est le pixel CSS, et le
|
// viewBox, ni bordure, ni remplissage : son unité est le pixel CSS, et le
|
||||||
// dessin vit dans un seul groupe <g data-vue>, dont l'attribut transform
|
// dessin vit dans un seul groupe <g data-vue>, dont l'attribut transform
|
||||||
// est la vue, {k, tx, ty}. Un zoom n'écrit que cet attribut sous la
|
// est la vue, {k, tx, ty}. Un zoom n'écrit que cet attribut sous la
|
||||||
|
|
@ -99,10 +102,12 @@
|
||||||
import Fantome from './Fantome.svelte';
|
import Fantome from './Fantome.svelte';
|
||||||
import { REPOS, cleFantome, geste, memoriser } from './gestes.js';
|
import { REPOS, cleFantome, geste, memoriser } from './gestes.js';
|
||||||
import Infobulle from './Infobulle.svelte';
|
import Infobulle from './Infobulle.svelte';
|
||||||
import { detailConflits, modeleDessin } from './modele.js';
|
import Legende from './Legende.svelte';
|
||||||
|
import { detailConflits, legendeConflits, modeleDessin } from './modele.js';
|
||||||
import PlanDessin from './PlanDessin.svelte';
|
import PlanDessin from './PlanDessin.svelte';
|
||||||
import RefusPlace from './RefusPlace.svelte';
|
import RefusPlace from './RefusPlace.svelte';
|
||||||
import {
|
import {
|
||||||
|
appartenanceDeCharge,
|
||||||
cadreDepuisRect,
|
cadreDepuisRect,
|
||||||
englobantDuTour,
|
englobantDuTour,
|
||||||
nomDeCharge,
|
nomDeCharge,
|
||||||
|
|
@ -147,6 +152,11 @@
|
||||||
const prefixe = $props.id();
|
const prefixe = $props.id();
|
||||||
const idPose = `${prefixe}-pose`;
|
const idPose = `${prefixe}-pose`;
|
||||||
const idInfobulle = `${prefixe}-infobulle`;
|
const idInfobulle = `${prefixe}-infobulle`;
|
||||||
|
const idLegende = `${prefixe}-legende`;
|
||||||
|
|
||||||
|
// La légende des conflits, ouverte ou non : un état de la vue, que le
|
||||||
|
// bouton de la barre bascule ; rien ne s'écrit.
|
||||||
|
let legendeOuverte = $state(false);
|
||||||
|
|
||||||
const charge = $derived($seance.charge);
|
const charge = $derived($seance.charge);
|
||||||
const tours = $derived(charge.evenement.tours);
|
const tours = $derived(charge.evenement.tours);
|
||||||
|
|
@ -154,6 +164,9 @@
|
||||||
const tour = $derived(etat.tour);
|
const tour = $derived(etat.tour);
|
||||||
const englobantPlan = $derived(englobantDuTour(etat));
|
const englobantPlan = $derived(englobantDuTour(etat));
|
||||||
const nomDe = $derived(nomDeCharge(charge));
|
const nomDe = $derived(nomDeCharge(charge));
|
||||||
|
const appartenanceDe = $derived(appartenanceDeCharge(charge));
|
||||||
|
// Les règles actives des réglages, que la légende montre.
|
||||||
|
const legende = $derived(legendeConflits(charge.reglages));
|
||||||
const selection = $derived($ui.selection);
|
const selection = $derived($ui.selection);
|
||||||
const pose = $derived($ui.pose);
|
const pose = $derived($ui.pose);
|
||||||
// La personne mise en évidence, que la barre de vue nomme : {id, nom}, nom
|
// La personne mise en évidence, que la barre de vue nomme : {id, nom}, nom
|
||||||
|
|
@ -197,7 +210,7 @@
|
||||||
const etatTable = etat.tables.find(({ id }) => id === infobulleTable);
|
const etatTable = etat.tables.find(({ id }) => id === infobulleTable);
|
||||||
const dessin = modele.tables.find(({ id }) => id === infobulleTable);
|
const dessin = modele.tables.find(({ id }) => id === infobulleTable);
|
||||||
if (etatTable === undefined || dessin === undefined) return null;
|
if (etatTable === undefined || dessin === undefined) return null;
|
||||||
const detail = detailConflits(etatTable, nomDe);
|
const detail = detailConflits(etatTable, nomDe, appartenanceDe);
|
||||||
if (detail === null) return null;
|
if (detail === null) return null;
|
||||||
const { centre, rayonEmprise } = dessin.geometrie;
|
const { centre, rayonEmprise } = dessin.geometrie;
|
||||||
const { x, y } = ecranDepuisDessin(vue, origineSvg, centre.x + rayonEmprise, centre.y - rayonEmprise);
|
const { x, y } = ecranDepuisDessin(vue, origineSvg, centre.x + rayonEmprise, centre.y - rayonEmprise);
|
||||||
|
|
@ -712,7 +725,20 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="plan">
|
<div class="plan">
|
||||||
<BarreVue {tour} {tours} {zoomer} {ajuster} {evidence} {source} />
|
<BarreVue
|
||||||
|
{tour}
|
||||||
|
{tours}
|
||||||
|
{zoomer}
|
||||||
|
{ajuster}
|
||||||
|
{evidence}
|
||||||
|
{source}
|
||||||
|
legende={legendeOuverte}
|
||||||
|
basculerLegende={() => (legendeOuverte = !legendeOuverte)}
|
||||||
|
{idLegende}
|
||||||
|
/>
|
||||||
|
{#if legendeOuverte}
|
||||||
|
<Legende entrees={legende} id={idLegende} />
|
||||||
|
{/if}
|
||||||
<div class="surface" data-surface bind:this={surface}>
|
<div class="surface" data-surface bind:this={surface}>
|
||||||
<BandeauPlan bandeau={modele === null ? [] : modele.bandeau} />
|
<BandeauPlan bandeau={modele === null ? [] : modele.bandeau} />
|
||||||
<!-- Le plan est un composant interactif à deux dimensions, que le
|
<!-- Le plan est un composant interactif à deux dimensions, que le
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,15 @@
|
||||||
// le premier disque du groupe reste la chaise.
|
// le premier disque du groupe reste la chaise.
|
||||||
//
|
//
|
||||||
// Une table en conflit, quand le plan signale, porte un contour épaissi
|
// Une table en conflit, quand le plan signale, porte un contour épaissi
|
||||||
// (contourConflit) et une trame (trameConflit) par-dessus son plateau ; sa
|
// (contourConflit), commun aux trois règles, et, par-dessus son plateau,
|
||||||
// pastille porte le nombre de violations, signalée ou non (§ 7.5). Sur la
|
// une trame par règle violée (table.trames, dans l'ordre du moteur) : la
|
||||||
// trame, un socle [data-socle] du fond du plateau, centré sous la
|
// forme et la teinte de sa sorte (CONFLITS, Motifs.svelte), superposées
|
||||||
// pastille, la sépare des traits de la trame : la pastille se mesure
|
// quand elle en viole plusieurs. Sa pastille porte le nombre de
|
||||||
// contre le fond du plateau (§ 14.5), et la trame reste peinte autour.
|
// violations, signalée ou non (§ 7.5). Sur les trames, un socle
|
||||||
// Le socle précède la marque de gel, qui se peint sur lui.
|
// [data-socle] du fond du plateau, centré sous la pastille, la sépare de
|
||||||
|
// leurs traits : la pastille se mesure contre le fond du plateau (§ 14.5),
|
||||||
|
// et les trames restent peintes autour. Le socle précède la marque de gel,
|
||||||
|
// qui se peint sur lui.
|
||||||
//
|
//
|
||||||
// Le groupe de la table porte id="table-<id>", celui d'une chaise
|
// Le groupe de la table porte id="table-<id>", celui d'une chaise
|
||||||
// id="siege-<table>-<siège>" : la racine du plan, seul arrêt de tabulation,
|
// id="siege-<table>-<siège>" : la racine du plan, seul arrêt de tabulation,
|
||||||
|
|
@ -65,7 +68,6 @@
|
||||||
const poignee = $derived(table.geometrie.poignee);
|
const poignee = $derived(table.geometrie.poignee);
|
||||||
const signal = $derived(signaler && table.conflit);
|
const signal = $derived(signaler && table.conflit);
|
||||||
const hachure = $derived(`url(#${prefixe}-hachure)`);
|
const hachure = $derived(`url(#${prefixe}-hachure)`);
|
||||||
const trame = $derived(`url(#${prefixe}-trame)`);
|
|
||||||
// Les marques de désignation de la table, autour de l'emprise.
|
// Les marques de désignation de la table, autour de l'emprise.
|
||||||
const autour = $derived(formesTable({ ...table.geometrie, rayonEmprise: table.geometrie.rayonEmprise + ECART_DESIGNATION }));
|
const autour = $derived(formesTable({ ...table.geometrie, rayonEmprise: table.geometrie.rayonEmprise + ECART_DESIGNATION }));
|
||||||
const cadreFocus = $derived(carreAutour(table.geometrie.centre, table.geometrie.rayonEmprise + ECART_FOCUS));
|
const cadreFocus = $derived(carreAutour(table.geometrie.centre, table.geometrie.rayonEmprise + ECART_FOCUS));
|
||||||
|
|
@ -90,11 +92,13 @@
|
||||||
<rect class="plateau" class:conflit={signal} data-marque={signal ? 'contourConflit' : 'contourTable'} {...formes.plateau} />
|
<rect class="plateau" class:conflit={signal} data-marque={signal ? 'contourConflit' : 'contourTable'} {...formes.plateau} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if signal}
|
{#if signal}
|
||||||
{#if formes.ronde}
|
{#each table.trames as trame (trame.regle)}
|
||||||
<circle class="trame" data-marque="trameConflit" fill={trame} {...formes.plateau} />
|
{#if formes.ronde}
|
||||||
{:else}
|
<circle class="trame" data-marque={trame.marque} fill="url(#{prefixe}-trame-{trame.regle})" {...formes.plateau} />
|
||||||
<rect class="trame" data-marque="trameConflit" fill={trame} {...formes.plateau} />
|
{:else}
|
||||||
{/if}
|
<rect class="trame" data-marque={trame.marque} fill="url(#{prefixe}-trame-{trame.regle})" {...formes.plateau} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
<circle class="socle" data-socle cx={formes.pastille.x} cy={formes.pastille.y} r={RAYON_SOCLE_PASTILLE} />
|
<circle class="socle" data-socle cx={formes.pastille.x} cy={formes.pastille.y} r={RAYON_SOCLE_PASTILLE} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if table.gelee}
|
{#if table.gelee}
|
||||||
|
|
@ -180,8 +184,10 @@
|
||||||
stroke: var(--couleur-table-trait);
|
stroke: var(--couleur-table-trait);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Le contour d'une table en conflit : épaissi, de l'encre forte du plan ;
|
||||||
|
les teintes des règles sont sur les trames. */
|
||||||
.plateau.conflit {
|
.plateau.conflit {
|
||||||
stroke: var(--couleur-conflit);
|
stroke: var(--couleur-contour-fort);
|
||||||
stroke-width: 4;
|
stroke-width: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { constante } from '../../geometrie/constantes.js';
|
||||||
import { regimeCibles } from '../../geometrie/designation.js';
|
import { regimeCibles } from '../../geometrie/designation.js';
|
||||||
import { geometrieTable } from '../../geometrie/tables.js';
|
import { geometrieTable } from '../../geometrie/tables.js';
|
||||||
import { decalagesLignes, tronquer } from '../../geometrie/texte.js';
|
import { decalagesLignes, tronquer } from '../../geometrie/texte.js';
|
||||||
|
import { CONFLITS } from '../marques.js';
|
||||||
|
|
||||||
/** Largeur maximale de la barre de l'échelle graphique, en px d'écran. */
|
/** Largeur maximale de la barre de l'échelle graphique, en px d'écran. */
|
||||||
export const LARGEUR_ECHELLE_PX = 120;
|
export const LARGEUR_ECHELLE_PX = 120;
|
||||||
|
|
@ -289,6 +290,9 @@ function bandeauDe(toutesEnConflit, diagnostic) {
|
||||||
return entrees;
|
return entrees;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// La sorte de conflit de chaque règle du moteur (CONFLITS).
|
||||||
|
const SORTE_DE_REGLE = new Map(CONFLITS.map((sorte) => [sorte.regle, sorte]));
|
||||||
|
|
||||||
// Une chaise libre au siège s : celle qu'ajoute la poignée au-delà de la
|
// Une chaise libre au siège s : celle qu'ajoute la poignée au-delà de la
|
||||||
// capacité, que rien ne tient ni n'occupe.
|
// capacité, que rien ne tient ni n'occupe.
|
||||||
const chaiseLibre = (siege) => ({ siege, occupant: null, reservation: null, titre: null, remplissage: 'normal', marques: [] });
|
const chaiseLibre = (siege) => ({ siege, occupant: null, reservation: null, titre: null, remplissage: 'normal', marques: [] });
|
||||||
|
|
@ -314,6 +318,7 @@ function porteurDe(place, id, regime) {
|
||||||
function tableDessin(etatTable, { k, miseEnEvidence, nomDe, selection = null, actif = null }, places = etatTable.capacite) {
|
function tableDessin(etatTable, { k, miseEnEvidence, nomDe, selection = null, actif = null }, places = etatTable.capacite) {
|
||||||
const { id, numero, gelee, surchargee, conflits } = etatTable;
|
const { id, numero, gelee, surchargee, conflits } = etatTable;
|
||||||
const geometrie = geometrieTable(etatTable, places);
|
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 = regimeCibles(k, geometrie);
|
||||||
const pastille = conflits === null ? 0 : conflits.nombre;
|
const pastille = conflits === null ? 0 : conflits.nombre;
|
||||||
const etatChaises = chaisesA(etatTable, places);
|
const etatChaises = chaisesA(etatTable, places);
|
||||||
|
|
@ -342,6 +347,7 @@ function tableDessin(etatTable, { k, miseEnEvidence, nomDe, selection = null, ac
|
||||||
regime,
|
regime,
|
||||||
conflit: pastille > 0,
|
conflit: pastille > 0,
|
||||||
pastille,
|
pastille,
|
||||||
|
trames,
|
||||||
selection: selectionnee === 'table',
|
selection: selectionnee === 'table',
|
||||||
focus: active === 'table',
|
focus: active === 'table',
|
||||||
gelee,
|
gelee,
|
||||||
|
|
@ -362,7 +368,9 @@ function tableDessin(etatTable, { k, miseEnEvidence, nomDe, selection = null, ac
|
||||||
* Chaque table, dans l'ordre de l'état des places, porte sa géométrie
|
* Chaque table, dans l'ordre de l'état des places, porte sa géométrie
|
||||||
* (geometrieTable, P2), son régime des cibles au zoom k (regimeCibles) —
|
* (geometrieTable, P2), son régime des cibles au zoom k (regimeCibles) —
|
||||||
* aucune chaise dessinée en régime table —, son conflit et sa pastille, lus
|
* aucune chaise dessinée en régime table —, son conflit et sa pastille, lus
|
||||||
* dans conflits — null : ni conflit, ni pastille —, son nom accessible, et
|
* dans conflits — null : ni conflit, ni pastille —, ses trames — la sorte
|
||||||
|
* de conflit (CONFLITS, marques.js) de chaque règle violée, {regle, marque,
|
||||||
|
* jeton, forme}, dans l'ordre du moteur —, son nom accessible, et
|
||||||
* une ligne par siège, de 1 à la capacité, précédée de l'en-tête d'une
|
* une ligne par siège, de 1 à la capacité, précédée de l'en-tête d'une
|
||||||
* table gelée. Quand toutes les tables sont en conflit, aucune n'est
|
* table gelée. Quand toutes les tables sont en conflit, aucune n'est
|
||||||
* signalée — signalerConflits faux — et le bandeau énonce le diagnostic ;
|
* signalée — signalerConflits faux — et le bandeau énonce le diagnostic ;
|
||||||
|
|
@ -412,28 +420,65 @@ export function modeleDessin(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Le détail des conflits d'une table au tour montré (§ 7.1, § 7.3), que
|
* Le détail des conflits d'une table au tour montré (§ 7.1, § 7.3, § 7.5),
|
||||||
* l'infobulle énonce : son numéro, le nombre de violations, et chaque règle
|
* que l'infobulle énonce : son numéro, le nombre de violations, puis ses
|
||||||
* violée dans l'ordre du moteur (conflitsParTable, M1), sa clé de libellé
|
* lignes, dans l'ordre des règles du moteur (conflitsParTable, M1). Chaque
|
||||||
* plan.infobulle.<règle> et ses groupes nommés — une paire de noms pour
|
* ligne porte sa règle, sa clé de libellé plan.infobulle.<règle>, une
|
||||||
* séparer les appartenances et nouveaux voisins, un nom seul pour nouvelle
|
* appartenance et ses groupes nommés. Séparer les appartenances donne une
|
||||||
* table. Les noms sont des données : l'infobulle pose chacun dans son nœud.
|
* ligne par appartenance, dans l'ordre de sa première paire, qui nomme
|
||||||
* null quand les conflits ne s'évaluent pas ou qu'il n'y en a aucun.
|
* l'appartenance qu'ont en commun les deux personnes de chacune de ses
|
||||||
|
* paires — le moteur ne réunit que des personnes de même appartenance non
|
||||||
|
* nulle, la chaîne même que la liste porte — ; nouveaux voisins et nouvelle
|
||||||
|
* table, une ligne chacune, appartenance null, une paire de noms ou un nom
|
||||||
|
* seul par groupe. Les noms et l'appartenance sont des données :
|
||||||
|
* l'infobulle pose chacun dans son nœud. null quand les conflits ne
|
||||||
|
* s'évaluent pas ou qu'il n'y en a aucun.
|
||||||
*
|
*
|
||||||
* @param {Object} etatTable EtatTable (A5)
|
* @param {Object} etatTable EtatTable (A5)
|
||||||
* @param {(id: number) => string} nomDe
|
* @param {(id: number) => string} nomDe
|
||||||
* @returns {null|{numero: number, nombre: number, regles: Array<{cle: string, groupes: string[][]}>}}
|
* @param {(id: number) => string|null} appartenanceDe
|
||||||
|
* @returns {null|{numero: number, nombre: number,
|
||||||
|
* lignes: Array<{regle: string, cle: string, appartenance: string|null, groupes: string[][]}>}}
|
||||||
*/
|
*/
|
||||||
export function detailConflits({ numero, conflits }, nomDe) {
|
export function detailConflits({ numero, conflits }, nomDe, appartenanceDe) {
|
||||||
if (conflits === null || conflits.nombre === 0) return null;
|
if (conflits === null || conflits.nombre === 0) return null;
|
||||||
return {
|
const lignes = [];
|
||||||
numero,
|
for (const { regle, paires, personnes } of conflits.regles) {
|
||||||
nombre: conflits.nombre,
|
const cle = `plan.infobulle.${regle}`;
|
||||||
regles: conflits.regles.map((regle) => ({
|
if (regle !== 'separerAppartenances') {
|
||||||
cle: `plan.infobulle.${regle.regle}`,
|
lignes.push({ regle, cle, appartenance: null, groupes: (paires ?? personnes.map((id) => [id])).map((groupe) => groupe.map(nomDe)) });
|
||||||
groupes: (regle.paires ?? regle.personnes.map((id) => [id])).map((groupe) => groupe.map(nomDe)),
|
continue;
|
||||||
})),
|
}
|
||||||
};
|
const parAppartenance = new Map();
|
||||||
|
for (const paire of paires) {
|
||||||
|
const appartenance = appartenanceDe(paire[0]);
|
||||||
|
if (!parAppartenance.has(appartenance)) {
|
||||||
|
const ligne = { regle, cle, appartenance, groupes: [] };
|
||||||
|
parAppartenance.set(appartenance, ligne);
|
||||||
|
lignes.push(ligne);
|
||||||
|
}
|
||||||
|
parAppartenance.get(appartenance).groupes.push(paire.map(nomDe));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { numero, nombre: conflits.nombre, lignes };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La légende des conflits (§ 7.5) : la sorte de conflit (CONFLITS) de
|
||||||
|
* chaque règle active des réglages, dans l'ordre du moteur, avec la clé de
|
||||||
|
* son nom, plan.legende.<règle>, et celle de sa forme,
|
||||||
|
* plan.legende.forme.<forme>. Une règle inactive n'y paraît pas ; varier
|
||||||
|
* les appartenances, qui ne se localise pas à une table, n'a pas de sorte.
|
||||||
|
*
|
||||||
|
* @param {{separerAppartenances: boolean, nouveauxVoisins: boolean, nouvelleTable: boolean}} reglages
|
||||||
|
* @returns {Array<{regle: string, marque: string, jeton: string, forme: string, cle: string, cleForme: string}>}
|
||||||
|
*/
|
||||||
|
export function legendeConflits(reglages) {
|
||||||
|
return CONFLITS.filter(({ regle }) => reglages[regle] === true).map((sorte) => ({
|
||||||
|
...sorte,
|
||||||
|
cle: `plan.legende.${sorte.regle}`,
|
||||||
|
cleForme: `plan.legende.forme.${sorte.forme}`,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { serialiser } from '../../stockage/canonique.js';
|
||||||
import { analyser, configurationDepuisCharge, creerCharge } from '../../stockage/document.js';
|
import { analyser, configurationDepuisCharge, creerCharge } from '../../stockage/document.js';
|
||||||
import { planDepuisFichier, versFichier } from '../../stockage/placements.js';
|
import { planDepuisFichier, versFichier } from '../../stockage/placements.js';
|
||||||
import { TABLES_INTERFACE } from '../libelles.js';
|
import { TABLES_INTERFACE } from '../libelles.js';
|
||||||
|
import { CONFLITS } from '../marques.js';
|
||||||
import {
|
import {
|
||||||
ECART_ANNEAU,
|
ECART_ANNEAU,
|
||||||
ECART_DESIGNATION,
|
ECART_DESIGNATION,
|
||||||
|
|
@ -39,6 +40,7 @@ import {
|
||||||
detailConflits,
|
detailConflits,
|
||||||
executerSurPlace,
|
executerSurPlace,
|
||||||
formesTable,
|
formesTable,
|
||||||
|
legendeConflits,
|
||||||
libelleLongueur,
|
libelleLongueur,
|
||||||
ligneListe,
|
ligneListe,
|
||||||
modeleDessin,
|
modeleDessin,
|
||||||
|
|
@ -880,38 +882,175 @@ describe('modeleDessin : les options selection et actif marquent leur chaise, ou
|
||||||
|
|
||||||
// --- Le détail d'une table en conflit (I11) --------------------------------------
|
// --- Le détail d'une table en conflit (I11) --------------------------------------
|
||||||
|
|
||||||
|
// L'appartenance de chaque participant de la charge, par identifiant.
|
||||||
|
const appartenancesDe = (charge) => {
|
||||||
|
const appartenances = new Map(charge.participants.map(({ id, appartenance }) => [id, appartenance]));
|
||||||
|
return (id) => appartenances.get(id) ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
describe('detailConflits : le détail que rend le moteur, nommé (§ 7.1, § 7.3)', () => {
|
describe('detailConflits : le détail que rend le moteur, nommé (§ 7.1, § 7.3)', () => {
|
||||||
test('petite démonstration en conflit : chaque règle violée, ses paires ou ses personnes, dans l’ordre du moteur', () => {
|
// Les lignes attendues d'une table : séparer les appartenances, une ligne
|
||||||
|
// par appartenance, dans l'ordre de leur première paire ; les autres
|
||||||
|
// règles, une ligne chacune.
|
||||||
|
function lignesAttendues(table, nomDe, appartenanceDe) {
|
||||||
|
const lignes = [];
|
||||||
|
for (const regle of table.conflits.regles) {
|
||||||
|
const cle = `plan.infobulle.${regle.regle}`;
|
||||||
|
if (regle.regle !== 'separerAppartenances') {
|
||||||
|
const groupes = (regle.paires ?? regle.personnes.map((id) => [id])).map((groupe) => groupe.map(nomDe));
|
||||||
|
lignes.push({ regle: regle.regle, cle, appartenance: null, groupes });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const [a, b] of regle.paires) {
|
||||||
|
const appartenance = appartenanceDe(a);
|
||||||
|
assert.equal(appartenanceDe(b), appartenance);
|
||||||
|
const ligne = lignes.find((l) => l.regle === regle.regle && l.appartenance === appartenance);
|
||||||
|
if (ligne === undefined) lignes.push({ regle: regle.regle, cle, appartenance, groupes: [[nomDe(a), nomDe(b)]] });
|
||||||
|
else ligne.groupes.push([nomDe(a), nomDe(b)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lignes;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('petite démonstration en conflit : chaque règle violée, ses paires ou ses personnes, dans l’ordre du moteur ; chaque paire réunie nomme son appartenance', () => {
|
||||||
const charge = avecPropositions(petite('petite-conflit'));
|
const charge = avecPropositions(petite('petite-conflit'));
|
||||||
const nomDe = nomsDe(charge);
|
const nomDe = nomsDe(charge);
|
||||||
|
const appartenanceDe = appartenancesDe(charge);
|
||||||
let vus = 0;
|
let vus = 0;
|
||||||
|
let separees = 0;
|
||||||
for (let tour = 1; tour <= charge.evenement.tours; tour += 1) {
|
for (let tour = 1; tour <= charge.evenement.tours; tour += 1) {
|
||||||
for (const table of etatPlaces(charge, { tour, proposition: 1 }).tables) {
|
for (const table of etatPlaces(charge, { tour, proposition: 1 }).tables) {
|
||||||
const detail = detailConflits(table, nomDe);
|
const detail = detailConflits(table, nomDe, appartenanceDe);
|
||||||
if (table.conflits.nombre === 0) {
|
if (table.conflits.nombre === 0) {
|
||||||
assert.equal(detail, null);
|
assert.equal(detail, null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
vus += 1;
|
vus += 1;
|
||||||
assert.deepEqual(detail, {
|
assert.deepEqual(detail, { numero: table.numero, nombre: table.conflits.nombre, lignes: lignesAttendues(table, nomDe, appartenanceDe) });
|
||||||
numero: table.numero,
|
for (const ligne of detail.lignes) {
|
||||||
nombre: table.conflits.nombre,
|
assert.ok(typeof t(ligne.cle) === 'string' && t(ligne.cle).length > 0, ligne.cle);
|
||||||
regles: table.conflits.regles.map((regle) => ({
|
if (ligne.regle === 'separerAppartenances') {
|
||||||
cle: `plan.infobulle.${regle.regle}`,
|
separees += 1;
|
||||||
groupes: (regle.paires ?? regle.personnes.map((id) => [id])).map((groupe) => groupe.map(nomDe)),
|
assert.equal(typeof ligne.appartenance, 'string');
|
||||||
})),
|
}
|
||||||
});
|
}
|
||||||
for (const { cle } of detail.regles) assert.ok(typeof t(cle) === 'string' && t(cle).length > 0, cle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.ok(vus > 0, 'au moins une table en conflit');
|
assert.ok(vus > 0, 'au moins une table en conflit');
|
||||||
|
assert.ok(separees > 0, 'au moins une paire réunie');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('séparer les appartenances : une ligne par appartenance, ses paires dans l’ordre du moteur ; l’appartenance telle que la liste la porte', () => {
|
||||||
|
const noms = new Map([[1, 'Iris Ombrelle'], [2, 'Théo Pervenche'], [3, 'Ondine Lacasse'], [4, 'Grisaille'], [5, 'Basile Tourmaline']]);
|
||||||
|
const appartenances = new Map([[1, 'Club des Merles'], [2, 'cercle des Hérons '], [3, 'Club des Merles'], [4, 'cercle des Hérons '], [5, 'Club des Merles']]);
|
||||||
|
const table = {
|
||||||
|
numero: 7,
|
||||||
|
conflits: {
|
||||||
|
nombre: 6,
|
||||||
|
regles: [
|
||||||
|
{ regle: 'separerAppartenances', paires: [[1, 3], [1, 5], [2, 4], [3, 5]] },
|
||||||
|
{ regle: 'nouveauxVoisins', paires: [[1, 2]] },
|
||||||
|
{ regle: 'nouvelleTable', personnes: [4] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const detail = detailConflits(table, (id) => noms.get(id), (id) => appartenances.get(id));
|
||||||
|
assert.deepEqual(detail, {
|
||||||
|
numero: 7,
|
||||||
|
nombre: 6,
|
||||||
|
lignes: [
|
||||||
|
{
|
||||||
|
regle: 'separerAppartenances',
|
||||||
|
cle: 'plan.infobulle.separerAppartenances',
|
||||||
|
appartenance: 'Club des Merles',
|
||||||
|
groupes: [['Iris Ombrelle', 'Ondine Lacasse'], ['Iris Ombrelle', 'Basile Tourmaline'], ['Ondine Lacasse', 'Basile Tourmaline']],
|
||||||
|
},
|
||||||
|
{ regle: 'separerAppartenances', cle: 'plan.infobulle.separerAppartenances', appartenance: 'cercle des Hérons ', groupes: [['Théo Pervenche', 'Grisaille']] },
|
||||||
|
{ regle: 'nouveauxVoisins', cle: 'plan.infobulle.nouveauxVoisins', appartenance: null, groupes: [['Iris Ombrelle', 'Théo Pervenche']] },
|
||||||
|
{ regle: 'nouvelleTable', cle: 'plan.infobulle.nouvelleTable', appartenance: null, groupes: [['Grisaille']] },
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('conflits non évalués, ou nuls : null', () => {
|
test('conflits non évalués, ou nuls : null', () => {
|
||||||
const charge = petite();
|
const charge = petite();
|
||||||
const [table] = etatPlaces(charge, { tour: 1 }).tables;
|
const [table] = etatPlaces(charge, { tour: 1 }).tables;
|
||||||
assert.equal(detailConflits({ ...table, conflits: null }, nomsDe(charge)), null);
|
assert.equal(detailConflits({ ...table, conflits: null }, nomsDe(charge), appartenancesDe(charge)), null);
|
||||||
assert.equal(detailConflits({ ...table, conflits: { nombre: 0, regles: [] } }, nomsDe(charge)), null);
|
assert.equal(detailConflits({ ...table, conflits: { nombre: 0, regles: [] } }, nomsDe(charge), appartenancesDe(charge)), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Les trois sortes de conflit : trames et légende ----------------------------
|
||||||
|
|
||||||
|
describe('les trames de conflit : une par règle violée, sa marque et son jeton (§ 7.5, § 12.7)', () => {
|
||||||
|
const PAR_REGLE = new Map(CONFLITS.map((c) => [c.regle, c]));
|
||||||
|
|
||||||
|
test('petite démonstration en conflit, chaque tour : les trames d’une table sont les règles qu’elle viole, dans l’ordre du moteur ; aucune pour une table saine', () => {
|
||||||
|
const charge = avecPropositions(petite('petite-conflit'));
|
||||||
|
const sortes = new Set();
|
||||||
|
let multiples = 0;
|
||||||
|
let saines = 0;
|
||||||
|
for (let tour = 1; tour <= charge.evenement.tours; tour += 1) {
|
||||||
|
const etat = etatPlaces(charge, { tour, proposition: 1 });
|
||||||
|
const modele = modeleDessin(etat, options(charge));
|
||||||
|
assert.equal(modele.tables.length, etat.tables.length);
|
||||||
|
etat.tables.forEach((table, i) => {
|
||||||
|
const attendu = table.conflits.regles.map(({ regle }) => ({ ...PAR_REGLE.get(regle) }));
|
||||||
|
assert.deepEqual(modele.tables[i].trames, attendu, `tour ${tour}, table ${table.id}`);
|
||||||
|
for (const { regle } of attendu) sortes.add(regle);
|
||||||
|
if (attendu.length > 1) multiples += 1;
|
||||||
|
if (attendu.length === 0) saines += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert.deepEqual([...sortes].sort(), ['nouveauxVoisins', 'nouvelleTable', 'separerAppartenances']);
|
||||||
|
assert.ok(multiples > 0, 'une table au moins viole plusieurs règles');
|
||||||
|
assert.ok(saines > 0, 'une table au moins est saine');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('conflits non évalués : aucune trame', () => {
|
||||||
|
const charge = petite();
|
||||||
|
const etat = etatPlaces(charge, { tour: 1 });
|
||||||
|
const modele = modeleDessin({ ...etat, tables: etat.tables.map((table) => ({ ...table, conflits: null })) }, options(charge));
|
||||||
|
assert.ok(modele.tables.length > 0);
|
||||||
|
assert.ok(modele.tables.every(({ trames, conflit, pastille }) => trames.length === 0 && !conflit && pastille === 0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('legendeConflits : les règles actives, leur marque et leur libellé (§ 7.5)', () => {
|
||||||
|
const reglages = (actives) => ({
|
||||||
|
separerAppartenances: actives.includes('separerAppartenances'),
|
||||||
|
nouveauxVoisins: actives.includes('nouveauxVoisins'),
|
||||||
|
nouvelleTable: actives.includes('nouvelleTable'),
|
||||||
|
varierAppartenances: true,
|
||||||
|
attribuerSieges: false,
|
||||||
|
});
|
||||||
|
const entree = (regle) => {
|
||||||
|
const { marque, jeton, forme } = CONFLITS.find((c) => c.regle === regle);
|
||||||
|
return { regle, marque, jeton, forme, cle: `plan.legende.${regle}`, cleForme: `plan.legende.forme.${forme}` };
|
||||||
|
};
|
||||||
|
|
||||||
|
test('les trois règles actives, dans l’ordre du moteur', () => {
|
||||||
|
assert.deepEqual(legendeConflits(reglages(['nouvelleTable', 'separerAppartenances', 'nouveauxVoisins'])), [
|
||||||
|
entree('separerAppartenances'),
|
||||||
|
entree('nouveauxVoisins'),
|
||||||
|
entree('nouvelleTable'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une règle inactive n’y paraît pas ; aucune règle active, une légende vide ; varier les appartenances ne se localise pas', () => {
|
||||||
|
assert.deepEqual(legendeConflits(reglages(['separerAppartenances', 'nouvelleTable'])), [entree('separerAppartenances'), entree('nouvelleTable')]);
|
||||||
|
assert.deepEqual(legendeConflits(reglages(['nouveauxVoisins'])), [entree('nouveauxVoisins')]);
|
||||||
|
assert.deepEqual(legendeConflits(reglages([])), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chaque libellé de la légende existe', () => {
|
||||||
|
for (const { regle, forme } of CONFLITS) {
|
||||||
|
assert.ok(t(`plan.legende.${regle}`).length > 0, regle);
|
||||||
|
assert.ok(t(`plan.legende.forme.${forme}`).length > 0, forme);
|
||||||
|
}
|
||||||
|
for (const cle of ['plan.legende.titre', 'plan.legende.ouvrir', 'plan.legende.pastille', 'plan.legende.pastilleExemple', 'plan.legende.aucune']) {
|
||||||
|
assert.ok(t(cle).length > 0, cle);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1066,5 +1205,6 @@ describe('les clés que les panneaux composent existent (§ 14.6)', () => {
|
||||||
for (const action of actions) assert.equal(typeof t(`plan.action.${action}`), 'string', action);
|
for (const action of actions) assert.equal(typeof t(`plan.action.${action}`), 'string', action);
|
||||||
for (const action of menu) assert.equal(typeof t(`plan.menu.${action}`), 'string', action);
|
for (const action of menu) assert.equal(typeof t(`plan.menu.${action}`), 'string', action);
|
||||||
for (const regle of ['separerAppartenances', 'nouveauxVoisins', 'nouvelleTable']) assert.equal(typeof t(`plan.infobulle.${regle}`), 'string');
|
for (const regle of ['separerAppartenances', 'nouveauxVoisins', 'nouvelleTable']) assert.equal(typeof t(`plan.infobulle.${regle}`), 'string');
|
||||||
|
assert.equal(typeof t('plan.infobulle.finAppartenance'), 'string');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,18 @@ export function nomDeCharge(charge) {
|
||||||
return (id) => noms.get(id);
|
return (id) => noms.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La fonction appartenanceDe de l'infobulle (detailConflits) : l'identifiant
|
||||||
|
* d'un participant de la charge vers son appartenance, la chaîne même que la
|
||||||
|
* liste porte, ou null — sans appartenance, ou pour un identifiant que la
|
||||||
|
* charge n'a pas.
|
||||||
|
* @returns {(id: number) => string|null}
|
||||||
|
*/
|
||||||
|
export function appartenanceDeCharge(charge) {
|
||||||
|
const appartenances = new Map(charge.participants.map(({ id, appartenance }) => [id, appartenance]));
|
||||||
|
return (id) => appartenances.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ce que la barre de vue dit du placement que le plan montre (§ 2.1, étape
|
* Ce que la barre de vue dit du placement que le plan montre (§ 2.1, étape
|
||||||
* 11 ; § 5.8), lu sur l'état des places du tour : le placement retenu, les
|
* 11 ; § 5.8), lu sur l'état des places du tour : le placement retenu, les
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
import { creerCharge } from '../../stockage/document.js';
|
import { creerCharge } from '../../stockage/document.js';
|
||||||
import {
|
import {
|
||||||
FACTEUR_BOUTON,
|
FACTEUR_BOUTON,
|
||||||
|
appartenanceDeCharge,
|
||||||
cadreDepuisRect,
|
cadreDepuisRect,
|
||||||
englobantDuTour,
|
englobantDuTour,
|
||||||
nomDeCharge,
|
nomDeCharge,
|
||||||
|
|
@ -249,6 +250,15 @@ describe('l’englobant d’un tour, le tour montré, les noms', () => {
|
||||||
assert.equal(nomDe(1), 'Iris Ombrelle');
|
assert.equal(nomDe(1), 'Iris Ombrelle');
|
||||||
assert.equal(nomDe(2), 'Grisaille');
|
assert.equal(nomDe(2), 'Grisaille');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('appartenanceDeCharge : l’appartenance de chaque participant telle que la liste la porte, null sans appartenance ni participant', () => {
|
||||||
|
const charge = chargeDEpreuve();
|
||||||
|
charge.participants[0].appartenance = ' Cercle des Hérons';
|
||||||
|
const appartenanceDe = appartenanceDeCharge(charge);
|
||||||
|
assert.equal(appartenanceDe(1), ' Cercle des Hérons');
|
||||||
|
assert.equal(appartenanceDe(2), null);
|
||||||
|
assert.equal(appartenanceDe(99), null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sourceMontree : ce que la barre de vue dit du placement montré (§ 2.1 étape 11, § 5.8)', () => {
|
describe('sourceMontree : ce que la barre de vue dit du placement montré (§ 2.1 étape 11, § 5.8)', () => {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ import {
|
||||||
} from '../../test/navigateur/rendu.js';
|
} from '../../test/navigateur/rendu.js';
|
||||||
import { provenanceVisible } from './apropos.js';
|
import { provenanceVisible } from './apropos.js';
|
||||||
import { TABLES_INTERFACE } from './libelles.js';
|
import { TABLES_INTERFACE } from './libelles.js';
|
||||||
import { MARQUES } from './marques.js';
|
import { CONFLITS, MARQUES } from './marques.js';
|
||||||
|
|
||||||
const { t: tTemoin } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE], { langue: 'temoin' });
|
const { t: tTemoin } = creerTraduction([...TABLES_APPLICATION, ...TABLES_INTERFACE], { langue: 'temoin' });
|
||||||
const { ouverture: OUVERTURE, fermeture: FERMETURE } = MARQUE_TEMOIN;
|
const { ouverture: OUVERTURE, fermeture: FERMETURE } = MARQUE_TEMOIN;
|
||||||
|
|
@ -78,8 +78,9 @@ const TEXTES_NON_PEINTS = new Set(['script', 'style', 'template', 'noscript', 't
|
||||||
// Conteneurs SVG qui ne peignent qu'à travers une référence.
|
// Conteneurs SVG qui ne peignent qu'à travers une référence.
|
||||||
const CONTENEURS_DE_REFERENCE = 'defs, pattern, clipPath, mask, marker, symbol, linearGradient, radialGradient';
|
const CONTENEURS_DE_REFERENCE = 'defs, pattern, clipPath, mask, marker, symbol, linearGradient, radialGradient';
|
||||||
|
|
||||||
// Les marques que la teinte du conflit peint seules (§ 12.7, § 7.5).
|
// Les trames de conflit, et le jeton que chacune peint seule (§ 12.7, § 7.5) :
|
||||||
const MARQUES_DU_CONFLIT = new Set(['contourConflit', 'trameConflit']);
|
// une teinte de conflit ne peint que la trame de sa règle.
|
||||||
|
const JETON_DE_TRAME = new Map(CONFLITS.map(({ marque, jeton }) => [marque, jeton]));
|
||||||
|
|
||||||
const PICTOGRAPHIQUE = /\p{Extended_Pictographic}/u;
|
const PICTOGRAPHIQUE = /\p{Extended_Pictographic}/u;
|
||||||
const LETTRE = /\p{L}/u;
|
const LETTRE = /\p{L}/u;
|
||||||
|
|
@ -651,27 +652,49 @@ function peinturesDe(element) {
|
||||||
return peintures;
|
return peintures;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fautes de la teinte réservée sous racine (§ 12.7, § 7.5) : un élément
|
/** Fautes des teintes réservées sous racine (§ 12.7, § 7.5) : un élément
|
||||||
* rendu qui peint la couleur de conflit sans être une marque de conflit.
|
* rendu qui peint une teinte de conflit sans être la trame de sa règle —
|
||||||
* → { fautes, lus }, lus le nombre d'éléments rendus, hors marques de
|
* aucun autre élément, ni la trame d'une autre règle. teintes : [{ jeton,
|
||||||
* conflit, qui peignent au moins une couleur. */
|
* couleur }], la couleur résolue de chaque jeton de conflit dans le thème
|
||||||
export function fautesTeinte(racine, conflit) {
|
* courant. → { fautes, lus }, lus le nombre d'éléments rendus qui peignent
|
||||||
|
* au moins une couleur. */
|
||||||
|
export function fautesTeinte(racine, teintes) {
|
||||||
const fautes = [];
|
const fautes = [];
|
||||||
let lus = 0;
|
let lus = 0;
|
||||||
for (const element of [racine, ...racine.querySelectorAll('*')]) {
|
for (const element of [racine, ...racine.querySelectorAll('*')]) {
|
||||||
if (element.closest(CONTENEURS_DE_REFERENCE) !== null || !rendu(element)) continue;
|
if (element.closest(CONTENEURS_DE_REFERENCE) !== null || !rendu(element)) continue;
|
||||||
if (MARQUES_DU_CONFLIT.has(element.dataset?.marque)) continue;
|
const sienne = JETON_DE_TRAME.get(element.dataset?.marque) ?? null;
|
||||||
const peintures = peinturesDe(element);
|
const peintures = peinturesDe(element);
|
||||||
if (peintures.some(({ couleurs }) => couleurs.length > 0)) lus += 1;
|
if (peintures.some(({ couleurs }) => couleurs.length > 0)) lus += 1;
|
||||||
for (const { propriete, couleurs } of peintures) {
|
for (const { propriete, couleurs } of peintures) {
|
||||||
if (couleurs.some((c) => egales(c, conflit))) fautes.push(`${situer(element)} : ${propriete} ${rgb(conflit)}`);
|
for (const { jeton, couleur } of teintes) {
|
||||||
|
if (jeton !== sienne && couleurs.some((c) => egales(c, couleur))) fautes.push(`${situer(element)} : ${propriete} ${rgb(couleur)} (${jeton})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { fautes, lus };
|
return { fautes, lus };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Les teintes de conflit du thème courant : [{ jeton, couleur }]. */
|
||||||
|
const teintesDeConflit = () => CONFLITS.map(({ jeton }) => ({ jeton, couleur: couleurDeJeton(jeton) }));
|
||||||
|
|
||||||
// --- Jamais la seule couleur ------------------------------------------------------------
|
// --- Jamais la seule couleur ------------------------------------------------------------
|
||||||
|
|
||||||
|
// La géométrie d'un motif, sans ses couleurs : sa transformation, son
|
||||||
|
// carreau, et chacune de ses formes — balise, attributs de géométrie,
|
||||||
|
// épaisseur de trait. Deux trames qui ne diffèrent que par la couleur de
|
||||||
|
// leur motif ont la même.
|
||||||
|
const GEOMETRIE = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'width', 'height', 'd'];
|
||||||
|
function formeDuMotif(element) {
|
||||||
|
const id = /^url\("?#([^")]+)"?\)/.exec(getComputedStyle(element).fill)?.[1];
|
||||||
|
const motif = id === undefined ? null : element.ownerDocument.getElementById(id);
|
||||||
|
if (motif === null) return 'motif absent';
|
||||||
|
const formes = [...motif.querySelectorAll('*')].map((n) =>
|
||||||
|
[n.localName, ...GEOMETRIE.map((a) => n.getAttribute(a) ?? ''), getComputedStyle(n).strokeWidth].join(' '),
|
||||||
|
);
|
||||||
|
return `motif ${motif.getAttribute('patternTransform') ?? ''} ${motif.getAttribute('width')}×${motif.getAttribute('height')} [${formes.join(', ')}]`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* La signature non chromatique d'un état : pour chaque élément rendu des
|
* La signature non chromatique d'un état : pour chaque élément rendu des
|
||||||
* racines, hors des conteneurs de référence, sa balise, l'épaisseur et les
|
* racines, hors des conteneurs de référence, sa balise, l'épaisseur et les
|
||||||
|
|
@ -692,7 +715,7 @@ export function signature(racines) {
|
||||||
[
|
[
|
||||||
n.localName,
|
n.localName,
|
||||||
svg && s.stroke !== 'none' ? `trait ${s.strokeWidth} ${s.strokeDasharray}` : '',
|
svg && s.stroke !== 'none' ? `trait ${s.strokeWidth} ${s.strokeDasharray}` : '',
|
||||||
svg ? `remplissage ${s.fill === 'none' ? 'aucun' : /^url\(/.test(s.fill) ? 'motif' : 'aplat'}` : '',
|
svg ? `remplissage ${s.fill === 'none' ? 'aucun' : /^url\(/.test(s.fill) ? formeDuMotif(n) : 'aplat'}` : '',
|
||||||
svg ? '' : `bords ${s.borderTopStyle} ${s.borderTopWidth}`,
|
svg ? '' : `bords ${s.borderTopStyle} ${s.borderTopWidth}`,
|
||||||
texte,
|
texte,
|
||||||
].join('|'),
|
].join('|'),
|
||||||
|
|
@ -707,7 +730,7 @@ export function signature(racines) {
|
||||||
// signature, classé par l'état des places que l'application calcule.
|
// signature, classé par l'état des places que l'application calcule.
|
||||||
function releverEtats(m) {
|
function releverEtats(m) {
|
||||||
const racine = m.racine;
|
const racine = m.racine;
|
||||||
const etats = { saine: [], conflit: [], libre: [], reservee: [], nonPourvu: [], tour: [], tous: [] };
|
const etats = { saine: [], conflit: [], libre: [], reservee: [], nonPourvu: [], tour: [], tous: [], trames: Object.fromEntries(CONFLITS.map(({ marque }) => [marque, []])) };
|
||||||
const charge = m.seance.etat().charge;
|
const charge = m.seance.etat().charge;
|
||||||
if (charge === null || racine.querySelector('[data-plan]') === null) return etats;
|
if (charge === null || racine.querySelector('[data-plan]') === null) return etats;
|
||||||
const { tour, proposition } = m.ui.etat();
|
const { tour, proposition } = m.ui.etat();
|
||||||
|
|
@ -718,6 +741,10 @@ function releverEtats(m) {
|
||||||
const pastille = racine.querySelector(`[data-pastille="${table.id}"]`);
|
const pastille = racine.querySelector(`[data-pastille="${table.id}"]`);
|
||||||
const sig = signature([...propres, ...(pastille === null ? [] : [pastille])]);
|
const sig = signature([...propres, ...(pastille === null ? [] : [pastille])]);
|
||||||
etats[(table.conflits?.nombre ?? 0) > 0 ? 'conflit' : 'saine'].push({ ou: `table ${table.id}`, sig });
|
etats[(table.conflits?.nombre ?? 0) > 0 ? 'conflit' : 'saine'].push({ ou: `table ${table.id}`, sig });
|
||||||
|
// Chaque trame de conflit, par sa sorte.
|
||||||
|
for (const trame of groupe.querySelectorAll(CONFLITS.map(({ marque }) => `[data-marque="${marque}"]`).join(', '))) {
|
||||||
|
if (rendu(trame)) etats.trames[trame.dataset.marque].push({ ou: `trame de la table ${table.id}`, sig: signature([trame]) });
|
||||||
|
}
|
||||||
for (const place of table.chaises) {
|
for (const place of table.chaises) {
|
||||||
const element = racine.querySelector(`[data-siege="${table.id}:${place.siege}"]`);
|
const element = racine.querySelector(`[data-siege="${table.id}:${place.siege}"]`);
|
||||||
if (element === null) continue;
|
if (element === null) continue;
|
||||||
|
|
@ -829,7 +856,7 @@ function releverTheme(racine) {
|
||||||
try {
|
try {
|
||||||
const textes = releverTextes(racine);
|
const textes = releverTextes(racine);
|
||||||
const marques = releverMarques(racine);
|
const marques = releverMarques(racine);
|
||||||
return { textes, marques, teinte: fautesTeinte(racine, couleurDeJeton('--couleur-conflit')) };
|
return { textes, marques, teinte: fautesTeinte(racine, teintesDeConflit()) };
|
||||||
} finally {
|
} finally {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -942,10 +969,17 @@ describe('les gardes relèvent la faute qu’un fragment porte (§ 14.2)', () =>
|
||||||
expect(fautes[2]).toContain('1.00:1');
|
expect(fautes[2]).toContain('1.00:1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('teinte réservée : un texte peint de la couleur de conflit est relevé', () => {
|
test('teintes réservées : un texte peint d’une teinte de conflit, et la trame d’une règle peinte de la teinte d’une autre, sont relevés ; une trame de sa propre teinte passe', () => {
|
||||||
const { fautes, lus } = avecFragment('<p style="color:var(--couleur-conflit)">alerte</p>', (c) => fautesTeinte(c, couleurDeJeton('--couleur-conflit')));
|
const { fautes, lus } = avecFragment(
|
||||||
expect(lus).toBeGreaterThan(0);
|
'<p style="color:var(--couleur-conflit-voisins)">alerte</p>' +
|
||||||
expect(fautes).toHaveLength(1);
|
'<svg width="80" height="20"><rect data-marque="trameAppartenance" width="20" height="20" style="fill:var(--couleur-conflit-table)" />' +
|
||||||
|
'<rect data-marque="trameTable" x="40" width="20" height="20" style="fill:var(--couleur-conflit-table)" /></svg>',
|
||||||
|
(c) => fautesTeinte(c, teintesDeConflit()),
|
||||||
|
);
|
||||||
|
expect(lus).toBeGreaterThan(2);
|
||||||
|
expect(fautes).toHaveLength(2);
|
||||||
|
expect(fautes[0]).toContain('--couleur-conflit-voisins');
|
||||||
|
expect(fautes[1]).toMatch(/trameAppartenance.*--couleur-conflit-table/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('listes : une partie qui traverse une chaise loin de son centre est relevée ; une boîte qui n’entre que dans le coin d’un cercle passe', () => {
|
test('listes : une partie qui traverse une chaise loin de son centre est relevée ; une boîte qui n’entre que dans le coin d’un cercle passe', () => {
|
||||||
|
|
@ -1010,6 +1044,20 @@ describe('les gardes relèvent la faute qu’un fragment porte (§ 14.2)', () =>
|
||||||
expect(fautesPaire('vide', [], [{ ou: 'b', sig: '' }])).toHaveLength(1);
|
expect(fautesPaire('vide', [], [{ ou: 'b', sig: '' }])).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('jamais la seule couleur : deux trames dont les motifs ne diffèrent que par la couleur ont la même signature ; deux formes de motif, deux signatures', () => {
|
||||||
|
const motif = (id, angle, teinte) =>
|
||||||
|
`<pattern id="${id}" patternUnits="userSpaceOnUse" width="12" height="12" patternTransform="rotate(${angle})">` +
|
||||||
|
`<line x1="6" y1="0" x2="6" y2="12" style="stroke:${teinte};stroke-width:4" /></pattern>`;
|
||||||
|
const sigs = avecFragment(
|
||||||
|
`<svg width="120" height="40"><defs>${motif('m-a', -45, '#a00')}${motif('m-b', -45, '#0a0')}${motif('m-c', 45, '#a00')}</defs>` +
|
||||||
|
'<rect id="a" width="30" height="30" style="fill:url(#m-a)" /><rect id="b" x="40" width="30" height="30" style="fill:url(#m-b)" />' +
|
||||||
|
'<rect id="c" x="80" width="30" height="30" style="fill:url(#m-c)" /></svg>',
|
||||||
|
(c) => ['a', 'b', 'c'].map((id) => signature([c.querySelector(`#${id}`)])),
|
||||||
|
);
|
||||||
|
expect(sigs[0]).toBe(sigs[1]);
|
||||||
|
expect(sigs[0]).not.toBe(sigs[2]);
|
||||||
|
});
|
||||||
|
|
||||||
test('bandeau et formulaires : une version absente, un formulaire validé par le navigateur et un champ de fichier visible sont relevés', () => {
|
test('bandeau et formulaires : une version absente, un formulaire validé par le navigateur et un champ de fichier visible sont relevés', () => {
|
||||||
const bandeau = avecFragment('<header data-bandeau>Nom</header>', (c) => fautesBandeau(c, null, { version: 'version-epreuve', provenance: 'épreuve' }));
|
const bandeau = avecFragment('<header data-bandeau>Nom</header>', (c) => fautesBandeau(c, null, { version: 'version-epreuve', provenance: 'épreuve' }));
|
||||||
expect(bandeau).toHaveLength(2);
|
expect(bandeau).toHaveLength(2);
|
||||||
|
|
@ -1146,7 +1194,7 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => {
|
||||||
aucuneFaute([...new Set(fautes)], 'atténuations');
|
aucuneFaute([...new Set(fautes)], 'atténuations');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('jamais la seule couleur : table saine et en conflit, place libre, réservée et à titre non pourvu, portée tour et tous, cadre en lecture, en écriture et bloqué', () => {
|
test('jamais la seule couleur : table saine et en conflit, les trois sortes de conflit, place libre, réservée et à titre non pourvu, portée tour et tous, cadre en lecture, en écriture et bloqué', () => {
|
||||||
const plan = atteints().find((r) => r.nom === 'evenement.plan.ecriture');
|
const plan = atteints().find((r) => r.nom === 'evenement.plan.ecriture');
|
||||||
expect(plan, 'le plan en écriture relevé').toBeDefined();
|
expect(plan, 'le plan en écriture relevé').toBeDefined();
|
||||||
const e = plan.etats;
|
const e = plan.etats;
|
||||||
|
|
@ -1157,6 +1205,11 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => {
|
||||||
...fautesPaire('place réservée / titre non pourvu', e.reservee, e.nonPourvu),
|
...fautesPaire('place réservée / titre non pourvu', e.reservee, e.nonPourvu),
|
||||||
...fautesPaire('portée tour / tous', e.tour, e.tous),
|
...fautesPaire('portée tour / tous', e.tour, e.tous),
|
||||||
];
|
];
|
||||||
|
// Les trois sortes de conflit, sur le plan qui les montre ensemble.
|
||||||
|
const trois = atteints().find((r) => r.nom === 'plan.troisConflits');
|
||||||
|
expect(trois, 'le plan des trois sortes de conflit relevé').toBeDefined();
|
||||||
|
const [a, b, c] = CONFLITS.map(({ marque }) => trois.etats.trames[marque]);
|
||||||
|
fautes.push(...fautesPaire('trame appartenance / voisins', a, b), ...fautesPaire('trame appartenance / table', a, c), ...fautesPaire('trame voisins / table', b, c));
|
||||||
const cadres = ['lecture', 'ecriture', 'bloque'].map((mode) => {
|
const cadres = ['lecture', 'ecriture', 'bloque'].map((mode) => {
|
||||||
const r = atteints().find((x) => x.nom === `evenement.plan.${mode}`);
|
const r = atteints().find((x) => x.nom === `evenement.plan.${mode}`);
|
||||||
return { ou: `cadre ${mode}`, sig: r?.cadre ?? null };
|
return { ou: `cadre ${mode}`, sig: r?.cadre ?? null };
|
||||||
|
|
@ -1168,12 +1221,12 @@ describe('chaque écran de l’itération (§ 14.5, § 14.6, § 19.11)', () => {
|
||||||
aucuneFaute(fautes, 'jamais la seule couleur');
|
aucuneFaute(fautes, 'jamais la seule couleur');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('teinte réservée : la couleur calculée de conflit ne peint que les marques de conflit, dans les deux thèmes', () => {
|
test('teintes réservées : chaque teinte de conflit calculée ne peint que la trame de sa règle, dans les deux thèmes', () => {
|
||||||
for (const theme of THEMES) {
|
for (const theme of THEMES) {
|
||||||
expect(atteints().reduce((n, r) => n + r[theme].teinte.lus, 0), `éléments peints lus [${theme}]`).toBeGreaterThan(0);
|
expect(atteints().reduce((n, r) => n + r[theme].teinte.lus, 0), `éléments peints lus [${theme}]`).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
const fautes = atteints().flatMap((r) => THEMES.flatMap((theme) => r[theme].teinte.fautes.map((f) => `${r.nom} [${theme}] — ${f}`)));
|
const fautes = atteints().flatMap((r) => THEMES.flatMap((theme) => r[theme].teinte.fautes.map((f) => `${r.nom} [${theme}] — ${f}`)));
|
||||||
aucuneFaute([...new Set(fautes)], 'teinte réservée');
|
aucuneFaute([...new Set(fautes)], 'teintes réservées');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('aucun emoji au rendu', () => {
|
test('aucun emoji au rendu', () => {
|
||||||
|
|
|
||||||
|
|
@ -453,6 +453,31 @@ export const ECRANS = Object.freeze([
|
||||||
return m.racine;
|
return m.racine;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
nom: 'plan.troisConflits',
|
||||||
|
mode: 'ecriture',
|
||||||
|
async atteindre(contexte) {
|
||||||
|
// La petite démonstration en conflit et une proposition, graine 3,
|
||||||
|
// montrée au tour 3, choisi au clavier : la table 1 viole les trois
|
||||||
|
// règles et superpose leurs trames, la table 2 est saine ; puis la
|
||||||
|
// légende des conflits, ouverte par la barre de vue.
|
||||||
|
let charge = demonstration('petite-conflit');
|
||||||
|
const configuration = configurationDepuisCharge(charge);
|
||||||
|
const propositions = rechercher(configuration, { graine: 3, nombre: 1, arret: 300, historique: 1000 });
|
||||||
|
charge = appliquerCommande('enregistrerGeneration', charge, { propositions, configuration, produitVersion: VERSION.affichee }).charge;
|
||||||
|
const m = await ouvrirEvenement(contexte, charge, { ecriture: true, section: 'plan' });
|
||||||
|
const choix = await element(m, '[data-champ="tour"]');
|
||||||
|
choix.focus();
|
||||||
|
for (let essai = 0; essai < choix.options.length && choix.value !== '3'; essai += 1) {
|
||||||
|
await touche('ArrowDown');
|
||||||
|
flushSync();
|
||||||
|
}
|
||||||
|
for (const marque of ['trameAppartenance', 'trameVoisins', 'trameTable']) await element(m, `[data-table="1"] [data-marque="${marque}"]`);
|
||||||
|
await cliquer(m, '[data-commande="legende"]');
|
||||||
|
await element(m, '[data-legende] [data-legende-regle="nouvelleTable"]');
|
||||||
|
return m.racine;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
nom: 'participants.refusChamp',
|
nom: 'participants.refusChamp',
|
||||||
mode: 'ecriture',
|
mode: 'ecriture',
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,11 @@ function localisateur(racine) {
|
||||||
// --- Garde 4 : les couleurs (§ 12.7, § 13.3, § 13.5) --------------------------
|
// --- Garde 4 : les couleurs (§ 12.7, § 13.3, § 13.5) --------------------------
|
||||||
|
|
||||||
const FEUILLE_DES_JETONS = 'src/interface/jetons.css';
|
const FEUILLE_DES_JETONS = 'src/interface/jetons.css';
|
||||||
const JETON_CONFLIT = '--couleur-conflit';
|
// La famille des teintes de conflit (§ 7.5, § 12.7) : --couleur-conflit et
|
||||||
|
// chaque --couleur-conflit-…, une par règle que le plan localise ; un nom
|
||||||
|
// qui les prolonge sans tiret n'en est pas.
|
||||||
|
const JETON_CONFLIT = /--couleur-conflit(?:-[\w-]+)?(?![\w-])/;
|
||||||
|
const litConflit = (valeur) => JETON_CONFLIT.test(valeur);
|
||||||
|
|
||||||
// Les couleurs nommées de CSS, et les couleurs du système : chacune est un
|
// Les couleurs nommées de CSS, et les couleurs du système : chacune est un
|
||||||
// littéral. transparent et currentColor ne portent aucune teinte et restent
|
// littéral. transparent et currentColor ne portent aucune teinte et restent
|
||||||
|
|
@ -530,8 +534,8 @@ function regleDeConflit(chemin, prelude) {
|
||||||
|
|
||||||
// Relevé des couleurs : littéral de couleur dans une feuille, un attribut
|
// Relevé des couleurs : littéral de couleur dans une feuille, un attribut
|
||||||
// style ou de couleur, une directive style:, ou une chaîne du script d'un
|
// style ou de couleur, une directive style:, ou une chaîne du script d'un
|
||||||
// composant ; déclaration de jeton hors de la feuille des jetons ; lecture du
|
// composant ; déclaration de jeton hors de la feuille des jetons ; lecture
|
||||||
// jeton de conflit hors des règles de conflit du plan. Les composants de
|
// d'une teinte de conflit hors des règles de conflit du plan. Les composants de
|
||||||
// src/interface et les feuilles .css de src/ sont lus, la feuille des jetons
|
// src/interface et les feuilles .css de src/ sont lus, la feuille des jetons
|
||||||
// exceptée.
|
// exceptée.
|
||||||
function releverCouleurs(racine) {
|
function releverCouleurs(racine) {
|
||||||
|
|
@ -549,7 +553,7 @@ function releverCouleurs(racine) {
|
||||||
const couleur = couleurLitterale(valeur);
|
const couleur = couleurLitterale(valeur);
|
||||||
if (couleur !== null) relever(debut, `couleur ${extrait(couleur)} dans ${propriete}`);
|
if (couleur !== null) relever(debut, `couleur ${extrait(couleur)} dans ${propriete}`);
|
||||||
if (propriete.startsWith('--')) relever(debut, `jeton déclaré ${propriete}`);
|
if (propriete.startsWith('--')) relever(debut, `jeton déclaré ${propriete}`);
|
||||||
if (valeur.includes(JETON_CONFLIT) && (enLigne || !regleDeConflit(chemin, prelude))) {
|
if (litConflit(valeur) && (enLigne || !regleDeConflit(chemin, prelude))) {
|
||||||
relever(debut, `jeton de conflit hors d'une règle de conflit du plan, ${enLigne ? 'en ligne' : extrait(selecteur)}`);
|
relever(debut, `jeton de conflit hors d'une règle de conflit du plan, ${enLigne ? 'en ligne' : extrait(selecteur)}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -581,7 +585,7 @@ function releverCouleurs(racine) {
|
||||||
for (const { valeur, debut } of valeurs) {
|
for (const { valeur, debut } of valeurs) {
|
||||||
const couleur = couleurLitterale(valeur);
|
const couleur = couleurLitterale(valeur);
|
||||||
if (couleur !== null) relever(debut, `couleur ${extrait(couleur)} dans ${noeud.name}`);
|
if (couleur !== null) relever(debut, `couleur ${extrait(couleur)} dans ${noeud.name}`);
|
||||||
if (valeur.includes(JETON_CONFLIT)) relever(debut, "jeton de conflit hors d'une règle de conflit du plan, en ligne");
|
if (litConflit(valeur)) relever(debut, "jeton de conflit hors d'une règle de conflit du plan, en ligne");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -594,7 +598,7 @@ function releverCouleurs(racine) {
|
||||||
if (COULEUR_ENTIERE.test(valeur)) {
|
if (COULEUR_ENTIERE.test(valeur)) {
|
||||||
relever(noeud.start, `couleur ${extrait(valeur)} dans le script`);
|
relever(noeud.start, `couleur ${extrait(valeur)} dans le script`);
|
||||||
}
|
}
|
||||||
if (valeur.includes(JETON_CONFLIT)) relever(noeud.start, "jeton de conflit hors d'une règle de conflit du plan, dans le script");
|
if (litConflit(valeur)) relever(noeud.start, "jeton de conflit hors d'une règle de conflit du plan, dans le script");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -890,8 +894,8 @@ const composant = ({ script = '', gabarit = '', style = '' }) =>
|
||||||
|
|
||||||
// Une feuille des jetons minimale, ses deux thèmes complets.
|
// Une feuille des jetons minimale, ses deux thèmes complets.
|
||||||
const JETONS_EPREUVE = [
|
const JETONS_EPREUVE = [
|
||||||
':root, :root[data-theme="clair"] { --couleur-a: #ffffff; --couleur-conflit: #b3261e; }',
|
':root, :root[data-theme="clair"] { --couleur-a: #ffffff; --couleur-conflit: #b3261e; --couleur-conflit-voisins: #c0189f; --couleur-conflit-table: #6a9400; }',
|
||||||
":root[data-theme='sombre'] { --couleur-a: #000000; --couleur-conflit: #ff8a80; }",
|
":root[data-theme='sombre'] { --couleur-a: #000000; --couleur-conflit: #ff8a80; --couleur-conflit-voisins: #c0689c; --couleur-conflit-table: #549c00; }",
|
||||||
'',
|
'',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|
@ -1158,6 +1162,33 @@ describe('sources de l’interface : les couleurs (§ 12.7, § 13.3, § 13.5)',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('la garde relève chaque teinte de conflit — toute la famille --couleur-conflit-… — lue hors d’une règle de conflit du plan, et l’admet dans une règle de conflit du plan', () => {
|
||||||
|
const fichiers = {
|
||||||
|
'src/interface/jetons.css': JETONS_EPREUVE,
|
||||||
|
'src/interface/accueil/Alerte.svelte': composant({
|
||||||
|
script: " const teinte = 'var(--couleur-conflit-table)';",
|
||||||
|
gabarit: '<p class="alerte">x</p>',
|
||||||
|
style: ' .alerte { color: var(--couleur-conflit-voisins); }',
|
||||||
|
}),
|
||||||
|
'src/interface/plan/Motif.svelte': composant({
|
||||||
|
gabarit: '<svg><line class="trait voisins-conflit" /><line class="trait table-conflit" /><circle class="trait" style="fill: var(--couleur-conflit-table)" /></svg>',
|
||||||
|
style: [
|
||||||
|
' .trait.voisins-conflit { stroke: var(--couleur-conflit-voisins); }',
|
||||||
|
' .trait.table-conflit { stroke: var(--couleur-conflit-table); }',
|
||||||
|
' .trait { fill: var(--couleur-conflit-voisins); }',
|
||||||
|
' .conflit-voisins { fill: var(--couleur-conflit-table); }',
|
||||||
|
].join('\n'),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
assert.deepEqual(avecArbre(fichiers, releverCouleurs), [
|
||||||
|
"src/interface/accueil/Alerte.svelte:2 jeton de conflit hors d'une règle de conflit du plan, dans le script",
|
||||||
|
"src/interface/accueil/Alerte.svelte:6 jeton de conflit hors d'une règle de conflit du plan, « .alerte »",
|
||||||
|
"src/interface/plan/Motif.svelte:1 jeton de conflit hors d'une règle de conflit du plan, en ligne",
|
||||||
|
"src/interface/plan/Motif.svelte:5 jeton de conflit hors d'une règle de conflit du plan, « .trait »",
|
||||||
|
"src/interface/plan/Motif.svelte:6 jeton de conflit hors d'une règle de conflit du plan, « .conflit-voisins »",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('la garde des thèmes relève un jeton qu’un thème ne redéfinit pas', () => {
|
test('la garde des thèmes relève un jeton qu’un thème ne redéfinit pas', () => {
|
||||||
const fichiers = {
|
const fichiers = {
|
||||||
'src/interface/jetons.css': [
|
'src/interface/jetons.css': [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue