[FIX] plan preview: drag ghost effect, page fallback, sheet border
The drag ghost now states what releasing does where the default said otherwise: a person with no reservation changes nothing, and the floor under the retained plan removes the person from the tour. A long event name no longer throws in the plan preview: the cut falls back to the orientation that still has room, and throws only when neither has. The on-screen sheet draws its edge as an inset outline, so the content zone is as wide as on paper. The unused page cut of the preview model is gone. Checked: node 3899, browser 769; captures 22 and 23 republished. --- FR --- [FIX] aperçu du plan : effet du fantôme, repli de page, bord de feuille Le fantôme du glisser dit ce que fait le relâcher là où le défaut disait autre chose : une personne sans réservation ne change rien, et le sol sous le retenu la retire du tour. Un long nom d'événement ne fait plus lever l'aperçu du plan : la découpe se replie sur l'orientation qui a la place, et ne lève que si aucune ne l'a. La feuille à l'écran trace son bord en contour intérieur : la zone de contenu a la largeur du papier. La découpe inutilisée du modèle de l'aperçu disparaît. Vérifié : node 3899, navigateur 769 ; captures 22 et 23 republiées. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
7c9b708cc9
commit
9a2c8c20f3
13 changed files with 276 additions and 65 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -98,7 +98,9 @@ function bords(n, utileMm, echelle, centre, etendue) {
|
|||
* le plan se lit sur une page : elle est alors d'une page.
|
||||
*
|
||||
* L'orientation est celle qui donne le moins de pages ; à pages égales, la
|
||||
* plus grande échelle ; à échelle égale aussi, le paysage.
|
||||
* plus grande échelle ; à échelle égale aussi, le paysage. Une orientation
|
||||
* sans surface utile ne concourt pas : une bande plus haute que la page
|
||||
* couchée laisse le portrait seul.
|
||||
*
|
||||
* Les tuiles, toutes de même taille, se rangent par rangées de haut en bas,
|
||||
* de gauche à droite dans chaque rangée ; rang est l'indice dans pages,
|
||||
|
|
@ -130,8 +132,9 @@ function bords(n, utileMm, echelle, centre, etendue) {
|
|||
* voisins: {haut: number|null, droite: number|null,
|
||||
* bas: number|null, gauche: number|null}},
|
||||
* tables: number[]}>}}
|
||||
* @throws {RangeError} papier ou mode inconnu ; marge, bande ou corps hors de leur domaine ; page
|
||||
* sans surface utile ; englobant plat, inversé ou non fini ; table sans hauteur de liste
|
||||
* @throws {RangeError} papier ou mode inconnu ; marge, bande ou corps hors de leur domaine ; aucune
|
||||
* orientation avec une surface utile ; englobant plat, inversé ou non fini ; table sans
|
||||
* hauteur de liste
|
||||
*/
|
||||
export function decouperPlan(
|
||||
englobant,
|
||||
|
|
@ -164,17 +167,23 @@ export function decouperPlan(
|
|||
const seuil = corpsMinMm / corpsDessinCm;
|
||||
const { largeur: petit, hauteur: grand } = PAPIERS[papier];
|
||||
|
||||
// Paysage d'abord : il garde la place à toute égalité.
|
||||
const candidats = [
|
||||
// Paysage d'abord : il garde la place à toute égalité. Une orientation
|
||||
// sans surface utile — une bande plus haute que la page couchée — n'est
|
||||
// pas candidate ; le portrait, plus haut, garde alors la sienne.
|
||||
const orientations = [
|
||||
{ orientation: 'paysage', feuille: { largeur: grand, hauteur: petit } },
|
||||
{ orientation: 'portrait', feuille: { largeur: petit, hauteur: grand } },
|
||||
].map(({ orientation, feuille }) => {
|
||||
const utile = { largeur: feuille.largeur - 2 * margeMm, hauteur: feuille.hauteur - 2 * margeMm - bandeMm };
|
||||
if (!(utile.largeur > 0 && utile.hauteur > 0)) {
|
||||
throw new RangeError(`page ${orientation} sans surface utile : ${utile.largeur} × ${utile.hauteur} mm`);
|
||||
}
|
||||
return { orientation, utile, ...disposer(utile, plan, seuil, mode) };
|
||||
});
|
||||
].map(({ orientation, feuille }) => ({
|
||||
orientation,
|
||||
utile: { largeur: feuille.largeur - 2 * margeMm, hauteur: feuille.hauteur - 2 * margeMm - bandeMm },
|
||||
}));
|
||||
const candidats = orientations
|
||||
.filter(({ utile }) => utile.largeur > 0 && utile.hauteur > 0)
|
||||
.map(({ orientation, utile }) => ({ orientation, utile, ...disposer(utile, plan, seuil, mode) }));
|
||||
if (candidats.length === 0) {
|
||||
const surfaces = orientations.map(({ orientation, utile }) => `${orientation} ${utile.largeur} × ${utile.hauteur} mm`);
|
||||
throw new RangeError(`aucune orientation n'a de surface utile : ${surfaces.join(', ')}`);
|
||||
}
|
||||
const retenu = candidats.reduce((meilleur, c) => {
|
||||
const pages = c.colonnes * c.rangees;
|
||||
const pagesMeilleur = meilleur.colonnes * meilleur.rangees;
|
||||
|
|
|
|||
|
|
@ -358,8 +358,34 @@ describe('decouperPlan : les refus', () => {
|
|||
assert.throws(() => decouperPlan(ok, { ...OPTIONS, corpsDessinCm: Number.NaN }), RangeError);
|
||||
assert.throws(() => decouperPlan(ok, { ...OPTIONS, margeMm: -1 }), RangeError);
|
||||
// A4 : 210 − 2 × 105 = 0 mm, en largeur du portrait et en hauteur du paysage.
|
||||
assert.throws(() => decouperPlan(ok, { ...OPTIONS, margeMm: 105 }), RangeError);
|
||||
assert.throws(() => decouperPlan(ok, { ...OPTIONS, bandeMm: 300 }), RangeError);
|
||||
assert.throws(() => decouperPlan(ok, { ...OPTIONS, margeMm: 105 }), { name: 'RangeError', message: /aucune orientation/ });
|
||||
// 297 − 20 − 300 < 0 : ni le portrait ni le paysage n'ont de hauteur.
|
||||
assert.throws(() => decouperPlan(ok, { ...OPTIONS, bandeMm: 300 }), { name: 'RangeError', message: /aucune orientation/ });
|
||||
// 297 − 20 − 277 = 0 : le portrait n'a plus de hauteur non plus.
|
||||
assert.throws(() => decouperPlan(ok, { ...OPTIONS, bandeMm: 277 }), { name: 'RangeError', message: /aucune orientation/ });
|
||||
});
|
||||
|
||||
test('une bande plus haute que le paysage : la découpe se replie sur le portrait, qui garde de la place', () => {
|
||||
// Bande de 195 mm, plus de la moitié d'une page couchée : le paysage a
|
||||
// 210 − 20 − 195 = −5 mm de hauteur, le portrait 297 − 20 − 195 = 82 mm.
|
||||
// Le plan large de 100 × 50 cm, qui préférerait le paysage, s'imprime en
|
||||
// portrait : e₁ = min(190/100, 82/50) = 1,64 < 2, sa grille au seuil est
|
||||
// ⌈200/190⌉ × ⌈100/82⌉ = 2 × 2, à min(380/100, 164/50) = 3,28.
|
||||
for (const mode of ['auto', 'unePage', 'decoupe']) {
|
||||
const plan = decouperPlan(boite(0, 0, 100, 50), { ...OPTIONS, bandeMm: 195, mode });
|
||||
assert.equal(plan.orientation, 'portrait', mode);
|
||||
for (const page of plan.pages) {
|
||||
proche(page.cadrage.hauteur * plan.echelle, 82, `${mode} : hauteur utile`, 1e-4);
|
||||
proche(page.cadrage.largeur * plan.echelle, 190, `${mode} : largeur utile`, 1e-4);
|
||||
}
|
||||
}
|
||||
assert.equal(decouperPlan(boite(0, 0, 100, 50), { ...OPTIONS, bandeMm: 195 }).pages.length, 4);
|
||||
// Lettre : 215,9 − 20 − 196 < 0 en paysage, 279,4 − 20 − 196 = 63,4 en portrait.
|
||||
const lettre = decouperPlan(boite(0, 0, 100, 50), { ...OPTIONS, papier: 'Lettre', bandeMm: 196, mode: 'unePage' });
|
||||
assert.equal(lettre.orientation, 'portrait');
|
||||
proche(lettre.pages[0].cadrage.hauteur * lettre.echelle, PAPIERS.Lettre.hauteur - 20 - 196, 'hauteur utile Lettre', 1e-4);
|
||||
// Le plan vide aussi : une page portrait.
|
||||
assert.equal(decouperPlan(null, { ...OPTIONS, bandeMm: 195 }).orientation, 'portrait');
|
||||
});
|
||||
|
||||
test('un englobant plat, inversé, non fini, ou hors des cotes représentables', () => {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
|
||||
[data-impression] [data-feuille] {
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
break-after: page;
|
||||
}
|
||||
|
|
|
|||
107
src/interface/impression/Feuille.navigateur.test.js
Normal file
107
src/interface/impression/Feuille.navigateur.test.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// La feuille de l'aperçu est la page imprimée (§ 11.7 : « ce n'est pas une
|
||||
// seconde mise en page : c'est la même ») : sa zone de contenu
|
||||
// [data-contenu] a la même largeur à l'écran et sous les médias
|
||||
// d'impression émulés, celle de la page moins ses deux marges. Le bord que
|
||||
// l'écran dessine autour de la feuille ne prend aucune place : la tuile du
|
||||
// plan, à la largeur utile, y tient sans être rognée, et les listes se
|
||||
// mesurent et se paginent à l'écran à la largeur du papier.
|
||||
//
|
||||
// App est montée par test/navigateur/ecrans.js sur le système de fichiers
|
||||
// simulé, au cadre de 1 366 × 700. Le projet navigateur emploie expect, et
|
||||
// test et describe de Vitest (§ 14.8).
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||
import { cdp } from 'vitest/browser';
|
||||
import { texteLivre } from '../../demo/livrees.js';
|
||||
import { constante } from '../../geometrie/constantes.js';
|
||||
import { analyser } from '../../stockage/document.js';
|
||||
import { fixerCadre } from '../../../test/navigateur/entrees.js';
|
||||
import { attendre, chargePrincipale, cliquer, creerContexte, ouvrirApercu } from '../../../test/navigateur/ecrans.js';
|
||||
import { MM_PAR_PX } from './modele.js';
|
||||
|
||||
// Images attendues au plus par étape : bien au-delà d'un rendu, et sous le
|
||||
// délai d'une épreuve, pour qu'une étape qui ne vient pas se nomme.
|
||||
const IMAGES = 300;
|
||||
|
||||
// Une feuille se mesure au dixième de millimètre (§ 11.7).
|
||||
const TOLERANCE_MM = 0.1;
|
||||
|
||||
let contexte = null;
|
||||
beforeEach(async () => {
|
||||
await fixerCadre(1366, 700);
|
||||
contexte = creerContexte();
|
||||
});
|
||||
afterEach(async () => {
|
||||
const erreurs = contexte.erreurs();
|
||||
await contexte.defaire();
|
||||
expect(erreurs, 'erreurs reçues par le collecteur').toEqual([]);
|
||||
});
|
||||
|
||||
const feuilles = (m) => [...m.racine.querySelectorAll('[data-impression] [data-feuille]')];
|
||||
|
||||
// Chaque feuille du document : sa largeur et celle de sa zone de contenu,
|
||||
// en px CSS, ce que le contenu porte au-delà de sa largeur visible, et le
|
||||
// bord tracé autour de la feuille — le style de son contour, s'il tient
|
||||
// dans le rectangle de la feuille (décalage + épaisseur ≤ 0), son ombre.
|
||||
const mesures = (m) =>
|
||||
feuilles(m).map((feuille) => {
|
||||
const contenu = feuille.querySelector('[data-contenu]');
|
||||
const style = getComputedStyle(feuille);
|
||||
return {
|
||||
rang: feuille.dataset.feuille,
|
||||
feuille: feuille.getBoundingClientRect().width,
|
||||
contenu: contenu.getBoundingClientRect().width,
|
||||
deborde: contenu.scrollWidth - contenu.clientWidth,
|
||||
bord: style.outlineStyle,
|
||||
bordDedans: parseFloat(style.outlineOffset) + parseFloat(style.outlineWidth) <= 0,
|
||||
ombre: style.boxShadow,
|
||||
};
|
||||
});
|
||||
|
||||
// Les mesures sous les médias d'impression émulés, puis l'écran rétabli.
|
||||
async function mesuresImprimees(m) {
|
||||
await cdp().send('Emulation.setEmulatedMedia', { media: 'print' });
|
||||
try {
|
||||
return mesures(m);
|
||||
} finally {
|
||||
await cdp().send('Emulation.setEmulatedMedia', { media: '' });
|
||||
}
|
||||
}
|
||||
|
||||
// La zone de contenu de chaque feuille, à l'écran et à l'impression : la
|
||||
// même largeur, celle de la feuille moins ses deux marges, et rien qui
|
||||
// déborde. L'écran trace le bord de la feuille ; le papier n'en porte ni
|
||||
// bord ni ombre.
|
||||
async function verifierLargeurs(m, cas) {
|
||||
const marge = constante('MARGE_PAGE_MM');
|
||||
const ecran = mesures(m);
|
||||
const papier = await mesuresImprimees(m);
|
||||
expect(ecran.length, `${cas} : des feuilles`).toBeGreaterThan(0);
|
||||
expect(papier.map(({ rang }) => rang), `${cas} : les mêmes feuilles`).toEqual(ecran.map(({ rang }) => rang));
|
||||
for (const [i, sur] of ecran.entries()) {
|
||||
const imprime = papier[i];
|
||||
const libelle = `${cas}, feuille ${sur.rang} : écran ${JSON.stringify(sur)}, impression ${JSON.stringify(imprime)}`;
|
||||
expect(Math.abs(sur.contenu - imprime.contenu) * MM_PAR_PX, libelle).toBeLessThanOrEqual(TOLERANCE_MM);
|
||||
expect(Math.abs((sur.feuille - sur.contenu) * MM_PAR_PX - 2 * marge), libelle).toBeLessThanOrEqual(TOLERANCE_MM);
|
||||
expect(sur.deborde, libelle).toBeLessThanOrEqual(0);
|
||||
expect([sur.bord, sur.bordDedans], libelle).toEqual(['solid', true]);
|
||||
expect([imprime.bord, imprime.ombre], libelle).toEqual(['none', 'none']);
|
||||
}
|
||||
}
|
||||
|
||||
describe('la feuille à l’écran est la feuille imprimée (§ 11.7)', () => {
|
||||
test('le plan de la principale : la zone de contenu a la même largeur à l’écran et à l’impression, et la tuile y tient', async () => {
|
||||
const m = await ouvrirApercu(contexte, chargePrincipale());
|
||||
await verifierLargeurs(m, 'plan, A4');
|
||||
});
|
||||
|
||||
test('la grande démonstration sur papier Lettre, plan puis liste d’accueil : la même largeur sur chaque feuille', async () => {
|
||||
const m = await ouvrirApercu(contexte, analyser(texteLivre('grande')).charge, { papier: 'Lettre' });
|
||||
await verifierLargeurs(m, 'plan, Lettre');
|
||||
await cliquer(m, '[data-ecran="apercu"] [data-commande="vue"][data-cible="accueil"]');
|
||||
await attendre('la liste d’accueil', () => m.racine.querySelector('[data-impression][data-vue-impression="accueil"] [data-feuille]'), IMAGES);
|
||||
await verifierLargeurs(m, 'accueil, Lettre');
|
||||
});
|
||||
});
|
||||
|
|
@ -13,6 +13,11 @@
|
|||
// [data-impression], qui porte les jetons du clair, et peint son fond du
|
||||
// papier et son texte de l'encre de la page.
|
||||
//
|
||||
// À l'écran, le bord de la feuille est un contour tracé sur son propre
|
||||
// rectangle, qui ne prend aucune place : la zone de contenu a la largeur
|
||||
// de la page moins ses marges, à l'écran comme sur le papier, et la tuile
|
||||
// du plan, à la largeur utile, y tient sans être rognée.
|
||||
//
|
||||
// La zone de contenu mesure la hauteur utile que la découpe reçoit : la
|
||||
// page, moins les marges, moins bandeMm, la bande mesurée sur la feuille
|
||||
// gabarit. Sans bandeMm, la feuille est le gabarit : sa hauteur suit ce
|
||||
|
|
@ -61,7 +66,8 @@
|
|||
flex-direction: column;
|
||||
margin: 0 auto var(--espace-5);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--couleur-page-filet);
|
||||
outline: 1px solid var(--couleur-page-filet);
|
||||
outline-offset: -1px;
|
||||
background-color: var(--couleur-papier);
|
||||
color: var(--couleur-page-texte);
|
||||
box-shadow: var(--ombre-2);
|
||||
|
|
|
|||
|
|
@ -5,14 +5,12 @@
|
|||
// ce qu'une vue imprime — papier, orientation, tour —, les dimensions d'une
|
||||
// page et de sa zone de contenu, la bande mesurée, la page nommée que la
|
||||
// feuille d'impression déclare pour chaque papier et chaque orientation, le
|
||||
// pied de cinq éléments et le filigrane d'une feuille, la source que
|
||||
// l'aperçu imprime et la découpe du plan. Le modèle ne rend que des
|
||||
// nombres, des codes, des clés et des données ; les composants les
|
||||
// traduisent et les posent.
|
||||
// pied de cinq éléments et le filigrane d'une feuille, et la source que
|
||||
// l'aperçu imprime. La découpe du plan en pages n'est pas ici : pagesDuPlan
|
||||
// (plan.js) l'appelle seule. Le modèle ne rend que des nombres, des codes,
|
||||
// des clés et des données ; les composants les traduisent et les posent.
|
||||
import { etatPlaces } from '../../application/places.js';
|
||||
import { decouperPlan } from '../../geometrie/pages.js';
|
||||
import { PAPIERS } from '../../geometrie/papiers.js';
|
||||
import { englobantImpression, geometrieTable } from '../../geometrie/tables.js';
|
||||
import { DECOUPES, VUES_IMPRESSION } from '../etat_ui.js';
|
||||
|
||||
/** Millimètres par pixel CSS : 96 px au pouce, 25,4 mm au pouce. */
|
||||
|
|
@ -243,16 +241,3 @@ export function sortieApercu(charge, { tour }, proposition) {
|
|||
tour: tour === 'tous' ? 'tous' : tourEtat,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* La découpe du plan d'un tour en pages (decouperPlan, § 11.7) : les
|
||||
* géométries des tables de l'état des places, à leur capacité, leur
|
||||
* englobant, et la bande mesurée sur la feuille gabarit.
|
||||
*
|
||||
* @param {Object} etat EtatPlaces
|
||||
* @param {{papier: string, bandeMm: number, decoupe: string}} options
|
||||
*/
|
||||
export function decoupeDuPlan(etat, { papier, bandeMm, decoupe }) {
|
||||
const geometries = etat.tables.map((table) => geometrieTable(table, table.capacite));
|
||||
return decouperPlan(englobantImpression(geometries), { papier, bandeMm, mode: decoupe, geometries });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ import { TABLES_APPLICATION } from '../../application/libelles.js';
|
|||
import { etatPlaces } from '../../application/places.js';
|
||||
import { MARQUE_TEMOIN, creerTraduction } from '../../application/traduction.js';
|
||||
import { constante } from '../../geometrie/constantes.js';
|
||||
import { decouperPlan } from '../../geometrie/pages.js';
|
||||
import { PAPIERS } from '../../geometrie/papiers.js';
|
||||
import { englobantImpression, geometrieTable } from '../../geometrie/tables.js';
|
||||
import { texteLivre } from '../../demo/livrees.js';
|
||||
import { analyser } from '../../stockage/document.js';
|
||||
import { TABLES_INTERFACE } from '../libelles.js';
|
||||
|
|
@ -25,7 +23,6 @@ import {
|
|||
clesPapier,
|
||||
cleDecoupe,
|
||||
cleVue,
|
||||
decoupeDuPlan,
|
||||
dimensionsPage,
|
||||
feuilleDe,
|
||||
filigraneDe,
|
||||
|
|
@ -220,7 +217,7 @@ describe('le filigrane BROUILLON (§ 11.7, § 14.5)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('la sortie de l’aperçu et la découpe du plan', () => {
|
||||
describe('la sortie de l’aperçu', () => {
|
||||
test('sans placement, la source des réservations, sans proposition ; le tour du plan borné aux tours de la soirée', () => {
|
||||
const charge = petite();
|
||||
const sortie = sortieApercu(charge, { tour: 9 }, null);
|
||||
|
|
@ -235,17 +232,16 @@ describe('la sortie de l’aperçu et la découpe du plan', () => {
|
|||
const sortie = sortieApercu(avecProposition(), { tour: 1 }, 1);
|
||||
assert.deepEqual([sortie.source, sortie.proposition, sortie.tour], ['proposition', 1, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
test('la découpe du plan reçoit la bande mesurée : une bande plus haute réduit la surface utile', () => {
|
||||
const etat = etatPlaces(petite(), { tour: 1 });
|
||||
const geometries = etat.tables.map((table) => geometrieTable(table, table.capacite));
|
||||
for (const bandeMm of [0, 12.5]) {
|
||||
const attendue = decouperPlan(englobantImpression(geometries), { papier: 'A4', bandeMm, mode: 'unePage', geometries });
|
||||
assert.deepEqual(decoupeDuPlan(etat, { papier: 'A4', bandeMm, decoupe: 'unePage' }), attendue, String(bandeMm));
|
||||
}
|
||||
const basse = decoupeDuPlan(etat, { papier: 'A4', bandeMm: 0, decoupe: 'unePage' });
|
||||
const haute = decoupeDuPlan(etat, { papier: 'A4', bandeMm: 150, decoupe: 'unePage' });
|
||||
assert.ok(haute.echelle < basse.echelle, `${haute.echelle} < ${basse.echelle}`);
|
||||
describe('la découpe du plan : une seule implémentation (§ 13.2)', () => {
|
||||
test('le modèle de l’aperçu ne découpe pas le plan : la vue du plan passe par pagesDuPlan (plan.js) seule', () => {
|
||||
// Un second appelant de decouperPlan découperait le même plan sur
|
||||
// d'autres géométries ou sans les listes, et deux aperçus d'un même
|
||||
// état ne compteraient plus les mêmes pages. Le texte du module, sans
|
||||
// ses commentaires, n'importe ni la découpe ni l'englobant d'impression.
|
||||
const source = readFileSync(new URL('./modele.js', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, '');
|
||||
assert.doesNotMatch(source, /geometrie\/pages\.js|\bdecouperPlan\b|\benglobantImpression\b/);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -99,13 +99,17 @@ export const TABLE = new Map([
|
|||
// courant modifie : l'état des données, non le geste (§ 5.8) ; une
|
||||
// personne réservée déplace sa réservation, dont il dit la portée, et
|
||||
// au-dessus du sol la retire (« ce tour ») ou se la voit refuser (« tous
|
||||
// les tours »). Le nom de la personne est une donnée, dans son propre nœud.
|
||||
// les tours ») ; une personne que rien ne tient n'y change rien ; sous le
|
||||
// retenu, le sol retire du tour. Le nom de la personne est une donnée,
|
||||
// dans son propre nœud.
|
||||
['plan.fantome.reservations', 'pose une réservation'],
|
||||
['plan.fantome.reservationTour', 'déplace sa réservation de ce tour'],
|
||||
['plan.fantome.reservationTous', 'déplace sa réservation, à tous les tours'],
|
||||
['plan.fantome.retireReservationTour', 'retire sa réservation de ce tour'],
|
||||
['plan.fantome.refuseTous', 'refusé : sa réservation vaut pour tous les tours'],
|
||||
['plan.fantome.rienNeChange', 'aucune réservation à retirer : rien ne change'],
|
||||
['plan.fantome.retenu', 'déplace dans le placement retenu'],
|
||||
['plan.fantome.retireDuTour', 'retire du tour : attend dans la réserve'],
|
||||
['plan.fantome.vueSeule', 'proposition en vue seule : rien ne change'],
|
||||
// La place où un refus ou un avertissement du plan se pose.
|
||||
['plan.refus.table', ({ numero }) => `Table ${numero}`],
|
||||
|
|
|
|||
|
|
@ -231,13 +231,24 @@ export function memoriser(controler) {
|
|||
}
|
||||
|
||||
// Les clés du libellé du fantôme, par cible de l'état des places : ce que
|
||||
// le dépôt modifie, ou rien sous une cible null.
|
||||
// le dépôt sur une chaise modifie, ou rien sous une cible null.
|
||||
const CLES_FANTOME = new Map([
|
||||
['reservations', 'plan.fantome.reservations'],
|
||||
['retenu', 'plan.fantome.retenu'],
|
||||
[null, 'plan.fantome.vueSeule'],
|
||||
]);
|
||||
|
||||
// Les mêmes, au-dessus du sol (commandeDeDepot, depot.js) : sous le retenu,
|
||||
// le dépôt retire la personne du tour (retirerDuTour) ; sous les
|
||||
// réservations, une personne qu'aucune réservation ne tient n'a rien à
|
||||
// retirer, et le dépôt ne rend rien ; sous une cible null, le refus de la
|
||||
// vue seule, comme sur une chaise.
|
||||
const CLES_FANTOME_SUR_LE_SOL = new Map([
|
||||
['reservations', 'plan.fantome.rienNeChange'],
|
||||
['retenu', 'plan.fantome.retireDuTour'],
|
||||
[null, 'plan.fantome.vueSeule'],
|
||||
]);
|
||||
|
||||
// Sous la cible 'reservations', la clé d'une personne que sa réservation
|
||||
// tient à sa chaise, par portée : son dépôt sur une chaise est
|
||||
// deplacerReservation, qui garde la portée.
|
||||
|
|
@ -257,13 +268,18 @@ const CLES_RESERVATION_SUR_LE_SOL = new Map([
|
|||
/**
|
||||
* La clé du libellé que le fantôme porte pendant le glissement d'une
|
||||
* personne (§ 5.8, § 7.1), qui dit ce que le relâcher au point courant
|
||||
* modifie : sous la cible 'reservations', « pose une réservation » pour une
|
||||
* personne que rien ne tient, sinon le déplacement de sa réservation et sa
|
||||
* portée — « tous les tours » s'y dit — ; au-dessus du sol, la même
|
||||
* réservation se dit retirée (« ce tour ») ou refusée (« tous les tours ») ;
|
||||
* « déplace dans le placement retenu » sous 'retenu' ; la vue seule sous une
|
||||
* cible null. Une cible, une portée ou un sol hors du contrat lève : il ne
|
||||
* prend aucun libellé par défaut.
|
||||
* modifie, celui que rend commandeDeDepot (depot.js) :
|
||||
* - sous la cible 'reservations', sur une chaise, « pose une réservation »
|
||||
* pour une personne que rien ne tient, sinon le déplacement de sa
|
||||
* réservation et sa portée — « tous les tours » s'y dit ; au-dessus du
|
||||
* sol, la même réservation se dit retirée (« ce tour ») ou refusée
|
||||
* (« tous les tours »), et « rien ne change » pour une personne que rien
|
||||
* ne tient ;
|
||||
* - sous 'retenu', « déplace dans le placement retenu » sur une chaise,
|
||||
* « retire du tour » au-dessus du sol ;
|
||||
* - la vue seule sous une cible null, partout.
|
||||
* Une cible, une portée ou un sol hors du contrat lève : il ne prend aucun
|
||||
* libellé par défaut.
|
||||
*
|
||||
* @param {'reservations'|'retenu'|null} cible EtatPlaces.cible
|
||||
* @param {'tous'|'tour'|null} [portee] la portée de la réservation qui tient
|
||||
|
|
@ -277,7 +293,7 @@ export function cleFantome(cible, portee = null, surLeSol = false) {
|
|||
throw new TypeError(`portée du fantôme hors du contrat : ${JSON.stringify(portee)}`);
|
||||
}
|
||||
if (typeof surLeSol !== 'boolean') throw new TypeError(`sol du fantôme hors du contrat : ${JSON.stringify(surLeSol)}`);
|
||||
if (cible !== 'reservations' || portee === null) return CLES_FANTOME.get(cible);
|
||||
if (cible !== 'reservations' || portee === null) return (surLeSol ? CLES_FANTOME_SUR_LE_SOL : CLES_FANTOME).get(cible);
|
||||
return (surLeSol ? CLES_RESERVATION_SUR_LE_SOL : CLES_RESERVATION_TENUE).get(portee);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { constante } from '../../geometrie/constantes.js';
|
|||
import { placesSousPoignee } from '../../geometrie/tables.js';
|
||||
import { deplacer, deplacerVue, dessinDepuisEcran, transformDe } from '../../geometrie/vue.js';
|
||||
import { TABLE as LIBELLES } from '../libelles/plan.js';
|
||||
import { commandeDeDepot, reservationTenue } from './depot.js';
|
||||
import { REPOS, cleFantome, geste, libellePlace, memoriser } from './gestes.js';
|
||||
|
||||
const SEUIL = constante('SEUIL_GLISSER_PX');
|
||||
|
|
@ -371,14 +372,11 @@ describe('les libellés d’un geste : le fantôme et la place d’une pose', ()
|
|||
test('cleFantome : au-dessus du sol, une réservation « ce tour » se dit retirée, une « tous les tours » refusée (§ 5.8)', () => {
|
||||
// Le dépôt sur le sol retire la première et refuse la seconde : le
|
||||
// fantôme dit ce qui arrivera au relâcher, non ce qui arrive sur une
|
||||
// chaise. Une personne que rien ne tient, et les autres cibles, gardent
|
||||
// leur libellé.
|
||||
// chaise. La vue seule garde son libellé.
|
||||
assert.equal(cleFantome('reservations', 'tour', true), 'plan.fantome.retireReservationTour');
|
||||
assert.equal(cleFantome('reservations', 'tous', true), 'plan.fantome.refuseTous');
|
||||
assert.equal(cleFantome('reservations', 'tour', false), 'plan.fantome.reservationTour');
|
||||
assert.equal(cleFantome('reservations', 'tous', false), 'plan.fantome.reservationTous');
|
||||
assert.equal(cleFantome('reservations', null, true), 'plan.fantome.reservations');
|
||||
assert.equal(cleFantome('retenu', null, true), 'plan.fantome.retenu');
|
||||
assert.equal(cleFantome(null, 'tous', true), 'plan.fantome.vueSeule');
|
||||
assert.match(LIBELLES.get('plan.fantome.retireReservationTour'), /retire/);
|
||||
assert.match(LIBELLES.get('plan.fantome.refuseTous'), /refusé/);
|
||||
|
|
@ -388,6 +386,70 @@ describe('les libellés d’un geste : le fantôme et la place d’une pose', ()
|
|||
}
|
||||
});
|
||||
|
||||
test('cleFantome : au-dessus du sol, le retenu se dit retiré du tour ; une personne que rien ne tient, rien ne change', () => {
|
||||
// Sous 'retenu', le dépôt sur le sol est retirerDuTour : la personne
|
||||
// attend dans la réserve (§ 7.1). Sous 'reservations', une personne
|
||||
// qu'aucune réservation ne tient n'a rien à retirer : le dépôt ne rend
|
||||
// rien, et le fantôme ne promet pas une réservation.
|
||||
for (const portee of [null, 'tour', 'tous']) {
|
||||
assert.equal(cleFantome('retenu', portee, true), 'plan.fantome.retireDuTour');
|
||||
assert.equal(cleFantome('retenu', portee, false), 'plan.fantome.retenu');
|
||||
}
|
||||
assert.equal(cleFantome('reservations', null, true), 'plan.fantome.rienNeChange');
|
||||
assert.equal(cleFantome('reservations', null, false), 'plan.fantome.reservations');
|
||||
assert.match(LIBELLES.get('plan.fantome.retireDuTour'), /retire du tour/);
|
||||
assert.match(LIBELLES.get('plan.fantome.retireDuTour'), /réserve/);
|
||||
assert.match(LIBELLES.get('plan.fantome.rienNeChange'), /rien ne change/);
|
||||
});
|
||||
|
||||
test('cleFantome dit l’effet du relâcher : chaque clé, sur chaque cible, portée et point, suit la commande de commandeDeDepot', () => {
|
||||
// Le fantôme et le dépôt lisent la même réservation (reservationTenue),
|
||||
// comme Plan.svelte : la personne 11, assise à la table 1, siège 1, sans
|
||||
// réservation, ou tenue « ce tour » ou « tous les tours ». Elle se
|
||||
// dépose sur une chaise libre, sur une chaise occupée, ou sur le sol.
|
||||
// L'effet est la commande exécutée, le code du refus rendu, ou « rien ».
|
||||
const EFFETS = new Map([
|
||||
['plan.fantome.reservations', ['reserver']],
|
||||
['plan.fantome.reservationTour', ['deplacerReservation']],
|
||||
['plan.fantome.reservationTous', ['deplacerReservation']],
|
||||
['plan.fantome.retireReservationTour', ['retirerReservation']],
|
||||
['plan.fantome.refuseTous', ['RESERVATION_TOUS_LES_TOURS']],
|
||||
['plan.fantome.retenu', ['deplacerDansRetenu', 'echangerDansRetenu']],
|
||||
['plan.fantome.retireDuTour', ['retirerDuTour']],
|
||||
['plan.fantome.rienNeChange', ['rien']],
|
||||
['plan.fantome.vueSeule', ['VUE_SEULE']],
|
||||
]);
|
||||
const RESERVATIONS = [null, { participant: 11, portee: 'tour', tour: 1 }, { participant: 11, portee: 'tous', tour: null }];
|
||||
const POINTS = [
|
||||
{ cible: { table: 2, siege: 1 }, surLeSol: false },
|
||||
{ cible: { table: 2, siege: 2 }, surLeSol: false },
|
||||
{ cible: null, surLeSol: true },
|
||||
];
|
||||
const depuis = { table: 1, siege: 1 };
|
||||
const vus = new Set();
|
||||
for (const cible of ['reservations', 'retenu', null]) {
|
||||
for (const reservation of RESERVATIONS) {
|
||||
const etat = {
|
||||
cible,
|
||||
refus: cible === null ? { code: 'VUE_SEULE', details: {} } : null,
|
||||
tables: [
|
||||
{ id: 1, chaises: [{ siege: 1, occupant: 11, reservation }, { siege: 2, occupant: 12, reservation: null }] },
|
||||
{ id: 2, chaises: [{ siege: 1, occupant: null, reservation: null }, { siege: 2, occupant: 13, reservation: null }] },
|
||||
],
|
||||
};
|
||||
for (const point of POINTS) {
|
||||
const cle = cleFantome(cible, reservationTenue(etat, 11, depuis)?.portee ?? null, point.surLeSol);
|
||||
const resultat = commandeDeDepot({ etatPlaces: etat, tour: 1, personne: 11, depuis, ...point });
|
||||
const effet = resultat === null ? 'rien' : (resultat.refus?.code ?? resultat.commande);
|
||||
const cas = JSON.stringify({ cible, reservation: reservation?.portee ?? null, point });
|
||||
assert.ok(EFFETS.get(cle).includes(effet), `${cas} : le fantôme dit ${cle}, le relâcher fait ${effet}`);
|
||||
vus.add(cle);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual([...vus].sort(), [...EFFETS.keys()].sort());
|
||||
});
|
||||
|
||||
test('cleFantome : une cible hors du contrat lève, au lieu de se dire « vue seule »', () => {
|
||||
for (const cible of ['reservation', 'Retenu', undefined, '', 0]) {
|
||||
assert.throws(() => cleFantome(cible), { name: 'TypeError', message: /^cible du fantôme hors du contrat/ });
|
||||
|
|
|
|||
Loading…
Reference in a new issue