[ADD] interface: plan view — zoom, framing, fit, scale, side panels
The drawing becomes a plan the operator can look around. One transform on
the drawing group holds the view, so the screen and the drawing share the
formula of src/geometrie; the wheel zooms about the pointer without
scrolling the page, « + », « − » and « Ajuster » bound the scale, and a
resize keeps the view inside its limits. Only a deliberate gesture saves
the framing, deferred, into reglages_locaux.json — in read mode and on a
blocked plan too. The side panels take their first form.
Checked: 2368 node and 378 browser tests.
--- FR ---
[ADD] interface : vue du plan — zoom, cadrage, ajuster, échelle
Le dessin devient un plan que l'opérateur parcourt. Une seule
transformation du groupe de dessin tient la vue : l'écran et le dessin
partagent la formule de src/geometrie ; la molette zoome autour du
pointeur sans faire défiler la page, « + », « − » et « Ajuster » bornent
l'échelle, et un redimensionnement garde la vue dans ses limites. Seul un
geste voulu enregistre le cadrage, en différé, dans reglages_locaux.json
— en lecture et sur un plan bloqué aussi. Les panneaux latéraux prennent
leur première forme.
Vérifié : 2368 node et 378 navigateur.
Assisted-by: Claude Opus 5.5
2026-10-07 09:09:45 -04:00
|
|
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
|
|
// La vue du plan côté interface (§ 7.2, § 8.5, § 13.3), sans DOM : ce que
|
|
|
|
|
|
// Plan.svelte décide de sa vue — à l'ouverture, après un changement de
|
|
|
|
|
|
// cadre, à la molette et aux boutons de zoom —, composé des seules
|
|
|
|
|
|
// fonctions de src/geometrie/vue.js. Le cadre est un paramètre, relu par
|
|
|
|
|
|
// l'appelant au moment du geste ; aucune fonction ne lit le DOM.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Une vue ne se calcule que dans un cadre où kAjuste rend un nombre : un
|
|
|
|
|
|
// cadre dont la largeur ou la hauteur ne dépasse pas 2 × MARGE_AJUSTEMENT_PX,
|
|
|
|
|
|
// nul compris — un plan monté caché —, ne fixe aucune borne, et bornesZoom
|
|
|
|
|
|
// lèverait sur null. Chaque fonction fait ce contrôle avant de borner.
|
|
|
|
|
|
import { nomAffiche } from '../../application/personnes.js';
|
|
|
|
|
|
import { englobant, geometrieTable } from '../../geometrie/tables.js';
|
|
|
|
|
|
import { ajuster, borner, bornesZoom, facteurMolette, kAjuste, zoomerSous } from '../../geometrie/vue.js';
|
|
|
|
|
|
|
|
|
|
|
|
/** Facteur de zoom des boutons de zoom, appliqué autour du centre du cadre. */
|
|
|
|
|
|
export const FACTEUR_BOUTON = 1.25;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Le Cadre du contrat depuis un DOMRect : gauche ← left, haut ← top,
|
|
|
|
|
|
* largeur ← width, hauteur ← height. Plan.svelte, les gestes et les
|
|
|
|
|
|
* épreuves n'en lisent pas d'autre.
|
|
|
|
|
|
* @param {{left: number, top: number, width: number, height: number}} rect
|
|
|
|
|
|
* @returns {{gauche: number, haut: number, largeur: number, hauteur: number}}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function cadreDepuisRect(rect) {
|
|
|
|
|
|
return { gauche: rect.left, haut: rect.top, largeur: rect.width, hauteur: rect.height };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Vue à l'ouverture : le cadrage mémorisé, son k ramené dans les bornes du
|
|
|
|
|
|
* cadre (borner) ; sans cadrage, l'ajustement. Null dès que kAjuste vaut
|
|
|
|
|
|
* null, cadrage mémorisé ou non. Le cadrage reçu n'est pas modifié.
|
|
|
|
|
|
* @param {{k: number, tx: number, ty: number}|null} cadrage
|
|
|
|
|
|
* @param {{minX: number, minY: number, maxX: number, maxY: number}|null} englobantPlan
|
|
|
|
|
|
* @param {{gauche: number, haut: number, largeur: number, hauteur: number}} cadre
|
|
|
|
|
|
* @returns {{k: number, tx: number, ty: number}|null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function vueInitiale(cadrage, englobantPlan, cadre) {
|
|
|
|
|
|
const k = kAjuste(englobantPlan, cadre);
|
|
|
|
|
|
if (k === null) return null;
|
|
|
|
|
|
if (cadrage === null) return ajuster(englobantPlan, cadre);
|
|
|
|
|
|
return borner(cadrage, cadre, bornesZoom(k));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Vue après un changement de cadre : k ajusté recalculé sur le nouveau
|
|
|
|
|
|
* cadre, k ramené dans ses bornes, le point de dessin au centre du cadre y
|
|
|
|
|
|
* restant. Quand kAjuste vaut null, la vue reçue elle-même, inchangée.
|
|
|
|
|
|
* @returns {{k: number, tx: number, ty: number}}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function vueApresCadre(vue, englobantPlan, cadre) {
|
|
|
|
|
|
const k = kAjuste(englobantPlan, cadre);
|
|
|
|
|
|
if (k === null) return vue;
|
|
|
|
|
|
return borner(vue, cadre, bornesZoom(k));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Vue après un cran de molette : zoom sous le pointeur (clientX, clientY),
|
|
|
|
|
|
* du facteur de facteurMolette(deltaY, deltaMode), aux bornes du cadre.
|
|
|
|
|
|
* Null quand il n'y a rien à zoomer : vue null, ou cadre où kAjuste vaut null.
|
|
|
|
|
|
* @param {{k: number, tx: number, ty: number}|null} vue
|
|
|
|
|
|
* @param {{clientX: number, clientY: number, deltaY: number, deltaMode: number}} evenement
|
|
|
|
|
|
* @returns {{k: number, tx: number, ty: number}|null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function zoomerMolette(vue, englobantPlan, cadre, { clientX, clientY, deltaY, deltaMode }) {
|
|
|
|
|
|
const k = vue === null ? null : kAjuste(englobantPlan, cadre);
|
|
|
|
|
|
if (k === null) return null;
|
|
|
|
|
|
return zoomerSous(vue, cadre, clientX, clientY, facteurMolette(deltaY, deltaMode), bornesZoom(k));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Vue après « Zoom + » (sens 1) ou « Zoom moins » (sens -1) : zoom de FACTEUR_BOUTON,
|
|
|
|
|
|
* ou de son inverse, autour du centre du cadre, aux bornes du cadre. Null
|
|
|
|
|
|
* quand il n'y a rien à zoomer, comme zoomerMolette. RangeError pour un
|
|
|
|
|
|
* autre sens.
|
|
|
|
|
|
* @param {1|-1} sens
|
|
|
|
|
|
* @returns {{k: number, tx: number, ty: number}|null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function zoomerBouton(vue, englobantPlan, cadre, sens) {
|
|
|
|
|
|
if (sens !== 1 && sens !== -1) throw new RangeError(`sens du zoom : 1 ou -1 attendu, reçu ${String(sens)}`);
|
|
|
|
|
|
const k = vue === null ? null : kAjuste(englobantPlan, cadre);
|
|
|
|
|
|
if (k === null) return null;
|
|
|
|
|
|
const facteur = sens === 1 ? FACTEUR_BOUTON : 1 / FACTEUR_BOUTON;
|
|
|
|
|
|
const x = cadre.gauche + cadre.largeur / 2;
|
|
|
|
|
|
const y = cadre.haut + cadre.hauteur / 2;
|
|
|
|
|
|
return zoomerSous(vue, cadre, x, y, facteur, bornesZoom(k));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* L'englobant des emprises des tables d'un état des places (EtatPlaces,
|
|
|
|
|
|
* A5), chacune à sa capacité courante, comme le modèle de dessin les pose
|
|
|
|
|
|
* (geometrieTable) ; null sans table. Il ne dépend ni du tour ni de k.
|
|
|
|
|
|
* @returns {{minX: number, minY: number, maxX: number, maxY: number}|null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function englobantDuTour(etat) {
|
|
|
|
|
|
return englobant(etat.tables.map((table) => geometrieTable(table, table.capacite)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Les tours que le sélecteur offre : 1 à tours, croissants.
|
|
|
|
|
|
* @param {number} tours entier ≥ 1
|
|
|
|
|
|
* @returns {number[]}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function toursOfferts(tours) {
|
|
|
|
|
|
return Array.from({ length: tours }, (_, rang) => rang + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Le tour que montre le plan : le tour demandé, ramené au dernier quand le
|
|
|
|
|
|
* nombre de tours a baissé depuis la demande — etatPlaces lève au-delà.
|
|
|
|
|
|
* @param {number} tour entier ≥ 1 @param {number} tours entier ≥ 1
|
|
|
|
|
|
* @returns {number}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function tourAffiche(tour, tours) {
|
|
|
|
|
|
return Math.min(tour, tours);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* La fonction nomDe du modèle de dessin : l'identifiant d'un participant de
|
|
|
|
|
|
* la charge vers son nom affiché (nomAffiche, F1).
|
|
|
|
|
|
* @returns {(id: number) => string}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function nomDeCharge(charge) {
|
|
|
|
|
|
const noms = new Map(charge.participants.map((participant) => [participant.id, nomAffiche(participant)]));
|
|
|
|
|
|
return (id) => noms.get(id);
|
|
|
|
|
|
}
|
[FIX] iteration 3 review: seat targets, gestures, warnings, settings
The wheel no longer zooms during a gesture, so a drop lands under the
ghost. A seat is a target the size of its pitch. A floor drop refuses to
remove an every-tour reservation. A seat reservation the retained plan
does not hold is flagged at the seat, with its swap as remedy. DERIVE names
the person; capacities have an upper bound; Generate waits for a pending
setting; a worker that fails to load shows a refusal; § 5.3 constraints
get their switches. Commands are not re-entrant and a double click does not
confirm. Checked: 2755 node, 550 browser tests, from the index alone.
--- FR ---
[FIX] revue de l'itération 3 : cibles, gestes, avertissements, réglages
La molette ne zoome plus pendant un geste : le dépôt tombe sous le fantôme.
Un siège est une cible de la taille de son pas. Un dépôt sur le sol refuse
de retirer une réservation de tous les tours. Une réservation à un siège
que le retenu ne tient pas est relevée au siège, l'échange en remède. La
dérive nomme la personne ; les capacités ont une borne ; Générer attend un
réglage en cours ; un travailleur qui ne se charge pas donne un refus ; le
§ 5.3 a ses bascules. Les commandes ne sont pas ré-entrantes et un double
clic ne confirme pas. Vérifié : 2755 node, 550 navigateur, index seul.
Assisted-by: Claude Opus 5.5
2026-10-07 17:50:35 -04:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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
|
|
|
|
|
|
* seules réservations — ou un retenu qui ne se lit pas, sous lequel le plan
|
|
|
|
|
|
* montre les réservations —, une proposition dont les gestes posent des
|
|
|
|
|
|
* réservations (avant la retenue), ou une proposition en vue seule (après).
|
|
|
|
|
|
* Quand une proposition est demandée (ui.proposition), la barre offre de ne
|
|
|
|
|
|
* plus la montrer : revenir au retenu quand il existe, aux réservations
|
|
|
|
|
|
* sinon ; une proposition demandée qu'un tour ne porte pas laisse le plan
|
|
|
|
|
|
* sur sa source réelle, et la commande reste offerte.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {{source: string, proposition: number|null, cible: string|null, refus: Object|null}} etat
|
|
|
|
|
|
* EtatPlaces du tour affiché
|
|
|
|
|
|
* @param {number|null} demandee la proposition demandée au plan
|
|
|
|
|
|
* @param {boolean} retenu la charge porte un placement retenu
|
|
|
|
|
|
* @returns {{libelle: {cle: string, details: Object}, revenir: string|null}}
|
|
|
|
|
|
* revenir : la clé du libellé de la commande, null sans proposition demandée
|
|
|
|
|
|
* @throws {TypeError} pour une source hors du contrat
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function sourceMontree(etat, demandee, retenu) {
|
|
|
|
|
|
const revenir = demandee === null ? null : retenu ? 'plan.source.revenirRetenu' : 'plan.source.revenirReservations';
|
|
|
|
|
|
const avec = (cle, details = {}) => ({ libelle: { cle, details }, revenir });
|
|
|
|
|
|
if (etat.source === 'retenu') return avec('plan.source.retenu');
|
|
|
|
|
|
if (etat.source === 'proposition') {
|
|
|
|
|
|
const cle = etat.cible === 'reservations' ? 'plan.source.propositionReservations' : 'plan.source.propositionVueSeule';
|
|
|
|
|
|
return avec(cle, { proposition: etat.proposition });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (etat.source === 'reservations') {
|
|
|
|
|
|
return avec(etat.refus?.code === 'RETENU_FAUTIF' ? 'plan.source.retenuFautif' : 'plan.source.reservations');
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new TypeError(`source de l’état des places hors du contrat : ${JSON.stringify(etat.source)}`);
|
|
|
|
|
|
}
|