Contrat interne du moteur, à jour au 2026-09-30 (v1.3). À lire avant toute évolution de
board.jsousymbols.js. L’état d’avancement du projet est dans ETAT.md.
Le tableau tient dans trois choses, et rien d’autre :
boards[] les planches (phases) ; une seule est « montée » à la fois
objs[] la scène de la planche montée : objets ordonnés, du fond vers le dessus
état d'entrée outil, forme, couleur, épaisseur, style de trait, sélection, geste en cours
past[] future[] l'historique de la planche montée, en instantanés complets
Une planche est { name, objs, nmPx, past, future }, plus grid pour une
mission importée sans carte (voir « Route dans la DTC »). load(i) la monte dans
les variables globales, stash() les y recopie. Tout le reste du moteur ne connaît
que la planche montée : ajouter les planches n’a changé aucune fonction de dessin ni
de geste.
Chaque objet porte sa vue : v absent = vue de dessus, v: 'p' = coupe. Il n’y a
qu’une liste objs, un historique, une sélection. draw() dessine le plan découpé à
planH(), puis la coupe translatée de profTop() ; locate() convertit un point
d’écran dans le repère de la vue touchée et fixe view ; topmost() ne regarde que
les objets de view. Tout le reste — poser, déplacer, étiqueter, coter, annuler —
fonctionne dans la coupe sans une ligne de plus.
Le repère de la coupe est fixe : hauteur PROF_H = 300 px, altitude 0 à
groundY(), plafond à PROF_TOP, distance 0 à PROF_L, profNmPx() pixels par NM
calculés sur une largeur de référence de 1 000 px. altAt(y) et yAt(ft) font la
conversion. Changer plafond ou largeur passe par reprof(), qui recale chaque
objet de la coupe : ses altitudes et distances restent vraies.
Sans carte, cam = null et le plan garde son repère écran. Avec une carte, les objets
du plan sont en pixels Mercator au zoom de référence 12 (REF_Z), et la caméra
{ x, y, z } dit quel point du terrain est au centre et à quel zoom. w2s() et
s2w() passent d’un repère à l’autre ; toScreen(o) fait une copie écran d’un
objet pour le dessiner et le désigner, en gardant l’original dans __src.
Règles qui en découlent :
topmost convertit le pointeur et l’objet) : les
tolérances en pixels restent des pixels écran à tout zoom ;rulerText et measText lisent __src, et
nmAt(y) donne les pixels par NM à la latitude du tracé (Mercator étire vers les
pôles) ;convertPlan() bascule le plan d’un repère à l’autre sans rien déplacer à l’écran ;restore() ne rend la caméra que si la
carte change : annuler un déplacement de symbole ne fait pas sauter la vue ;vp) et une caméra le temps de dessiner, après
avoir chargé les tuiles (preloadTiles). kbFit() dit ce que la page montre : le
cadre kframe s’il existe, qui remplit alors exactement la zone du plan ; sinon
l’emprise vue à l’écran (carte) ou tous les objets (sans carte). La page a les
proportions de la planchette DCS, 0,142 × 0,214 (KB_W, KB_H), et kbLayout()
y place l’en-tête, le pied et la coupe.bearing() rend un cap suffixé : V (vrai, sur une carte), M (magnétique), rien sur
une planche sans carte (le haut de l’écran n’est pas un nord). En mode magnétique,
declAt(x, y) donne la déclinaison au point mesuré : la saisie de la planche
(magDec) si elle existe, sinon WMM2025 (wmmDeclination, magnetic.js) sur une
carte, sinon null — et le cap reste alors au cap écran plutôt que d’être inventé.
Cap magnétique = cap vrai − déclinaison Est.
APP porte la version et les adresses (studio, FlightLedger, dépôt une fois publié) ;
SIGNATURE est apposée sur chaque PNG (signed()) et en pied de kneeboard. DEMO est
vrai quand l’adresse contient ?demo : boot() monte demoBoards() et commit()
n’écrit jamais dans le stockage — une démo ne doit pas écraser le tableau du visiteur.
La route n’est pas stockée dans la coupe. computeRoute() la reconstruit à chaque
draw() depuis les waypoints du plan triés par numéro : distance cumulée
(Σ longueur / nmPx), altitude (w.alt, sinon celle du précédent, sinon 10 000 ft).
Elle remplit aussi routeAlt, où le plan lit l’altitude à afficher sous chaque
waypoint. La seule donnée ajoutée est alt sur un waypoint ; le geste drag.m = 'alt'
et l’édition { t:'wpalt' } sont les deux seules façons de l’écrire.
Le canvas ne mémorise rien : il est entièrement redessiné à chaque draw() à
partir de objs. Aucun pixel n’est une source de vérité. C’est ce qui rend
l’annulation, le changement de thème et l’export PNG triviaux.
objs[]Douze types d’objets. Champs communs à tous : t (type), c (couleur CSS),
w (épaisseur de trait en pixels). Les objets tracés portent aussi ls : solid,
dash ou dot. Tout objet peut porter locked: true : il est ancré (§4). Tout
objet porte un uid, posé à sa première écriture (withUids() dans commit() et
load()) : « + phase » le garde, une copie Ctrl+D en reçoit un neuf. Un sym ou un
text peut porter hook: { to, u, v, da } : il est accroché au symbole d’uid
to (§4, Objets accrochés) ; sa position est alors déduite, pas saisie.
t |
Champs propres | Sens |
|---|---|---|
sym |
k, x, y, a, s, n, lbl, alt (waypoint), rng az (écran radar), mark (piste radar) |
forme de la palette : clé, position, angle en radians, échelle, numéro (waypoints), étiquette attachée ; échelle et azimut d’un écran ; marque ls ou dt2 d’un HAFU |
arrow |
x1,y1, x2,y2, cx,cy, bent, both, meas |
flèche : extrémités, point de contrôle quadratique, courbure assumée, double pointe, cote affichée |
line |
identiques à arrow |
trait, même géométrie sans pointe |
rect |
x1,y1, x2,y2 |
coins opposés |
circle |
x1,y1, x2,y2 |
centre puis un point du rayon |
stroke |
pts[[x,y]…] |
tracé libre au crayon |
zone |
pts[[x,y]…], lbl |
polygone hachuré fermé ; étiquette en haut |
ruler |
x1,y1, x2,y2 |
mesure : distance (px ou NM selon nmPx) et cap ; dans la coupe, distance au sol et écart d’altitude |
terrain |
pts[[x,y]…] |
coupe : crête du relief, remplie jusqu’au sol |
dome |
x1 (site), x2 (rayon), y2 (plafond), lbl |
coupe : enveloppe sol-air, demi-ellipse posée au sol |
block |
y1, y2, lbl |
coupe : tranche d’altitude pleine largeur |
text |
s, x, y |
s est la chaîne, x,y son ancre (ligne de base médiane) |
img |
el, x, y, w2, h2, w0, h0 |
image de fond : élément DOM, coin haut-gauche, taille courante, taille d’origine |
kframe |
x, y, w2 |
cadre du kneeboard, un par planche : coin haut-gauche et largeur ; sa hauteur suit les proportions de la zone du plan de la page (kbHeight()). Aide d’édition : jamais dessiné en présentation ni dans un export (bare) |
L’ordre du tableau est l’ordre de rendu. Une image ajoutée entre par unshift
(donc au fond) ; le bouton « premier plan » déplace un objet en fin de liste.
Un sym ne stocke aucune forme : seulement une clé vers SHAPES. La forme est
dessinée dans un repère normalisé [-1, 1], nez vers -y, puis :
translate(x, y) → rotate(a) → scale(SIZE × s)
SIZE = 34 est la demi-taille de référence. a = 0 pointe au nord. L’épaisseur de
trait est divisée par l’échelle, donc elle reste constante quelle que soit la
taille du symbole.
Toujours une courbe quadratique : x1,y1 → contrôle cx,cy → x2,y2. Une flèche
droite n’est pas un cas particulier, c’est une courbe dont le contrôle est au milieu.
Tant que bent est faux, déplacer une extrémité recentre le contrôle ; dès que
l’utilisateur a tiré la poignée centrale, bent passe à vrai et le contrôle ne bouge
plus tout seul. La pointe s’oriente sur la tangente (x2−cx, y2−cy), jamais sur la
corde — sinon elle pointerait de travers sur une courbe prononcée.
| Variable | Valeurs | Rôle |
|---|---|---|
tool |
sym select pen arrow line rect circle zone ruler text erase |
outil courant ; select à l’ouverture |
rpress |
{ sx, sy, moved } ou null |
appui du bouton droit en cours, décidé au relâché |
ls |
solid dash dot |
style de trait des prochains objets — et de la sélection |
nmPx |
nombre, 0 = non étalonné | pixels par mille nautique de la planche montée ; fait partie de l’historique |
tb |
1, ou plus pendant l’export | grossissement des textes |
unit |
nm km |
unité d’affichage des distances ; préférence globale, persistée |
measOn |
booléen | les prochains traits et flèches naissent cotés |
cam |
{ x, y, z } ou null |
caméra de la carte de la planche montée ; null = sans carte |
mapCfg |
{ theatre, style } ou null |
théâtre et fond de la planche montée |
showAF |
booléen | aérodromes DCS affichés ; persisté |
headRef |
true mag |
référence des caps affichés ; persistée |
magDec |
nombre ou null |
déclinaison saisie pour la planche (° Est positif) ; dans l’historique |
split |
booléen | écran partagé plan / coupe ; persisté |
prof |
{ ceil, range, linked, pane, rrng, raz, grid } |
plafond (ft), largeur (NM) et route liée de la planche montée ; vue du panneau du bas (fa18, f16, absente = coupe), échelle et balayage de la vue radar ; grid : mission importée sans carte, caps de grille ; dans l’historique |
view |
m p |
vue du geste en cours, fixée au pointerdown et gardée jusqu’au relâché |
symKey |
clé de SHAPES |
forme que posera l’outil sym |
color, width |
couleur FL, 2 / 4 / 8 | valeurs des prochains objets — et repeignent la sélection si elle existe |
dark |
booléen | fond du tableau ; n’affecte aucun objet |
sel |
objet ou null |
sélection courante (un seul objet) |
draft |
objet ou null |
objet en cours de tracé, pas encore dans objs |
drag |
{m, o, …} ou null |
geste en cours sur un objet existant |
Une seule source : nmPx, pixels par mille nautique de la planche. distText()
convertit (1 NM = 1,852 km) et répond en pixels tant que la planche n’est pas
étalonnée — jamais un chiffre inventé. bearing() donne le cap écran, nord en haut,
de 001° à 360°. Une cote de flèche mesure la longueur de la courbe
(curveLength(), 32 segments), la règle la distance droite ; toutes deux donnent le
cap du départ vers l’arrivée.
pointerdown applique un ordre de priorité strict. Le premier cas qui accepte
consomme l’événement :
−1. mode présentation → gauche : laser ; droit ou molette : la carte ; rien d'autre
0. bouton droit → noté (rpress) ; déplace la carte s'il y en a une ;
relâché à moins de 5 px : retour à la sélection
0. bouton molette → déplace la carte s'il y en a une, sinon rien
0. accroche en attente (🔗, hooking) → le symbole touché devient l'hôte (hookTo), ou refus
1. une poignée de la sélection est sous le doigt → drag (voir table ci-dessous)
— sauf pendant le tracé d'une zone ; un objet ancré n'a pas de poignée
2. outil zone → ajoute un sommet / referme
3. outil gomme → supprime l'objet touché, hors image de fond et ancrés
4. outil sélection → désigne, un objet libre avant un ancré ; libre :
déplacement ; ancré ou vide : la carte
5. outil symbole, sur une forme ou un texte libre → saisit et déplace (grab)
6. outil symbole, dans le vide ou sur un ancré → pose la forme, puis drag 'place'
7. outil texte, sur forme / zone / texte → édite l'étiquette ou le texte (named)
8. outil texte, dans le vide → ouvre le champ flottant
9. autres outils (dont règle) → commence un draft
Le double-clic, quel que soit l'outil, referme la zone en cours ou ouvre l'édition de
l'étiquette de ce qu'on touche.
Les poignées passent avant tout : sans cela, on ne pourrait plus courber une flèche qui recouvre un autre objet.
grab() ne retient que les sym et les text : flèches, traits et carte de fond
restent traversables, pour qu’on puisse poser un symbole sur une flèche ou sur la
carte. Une forme posée est lâchée en fin de geste (sel = null) : sinon la
couleur choisie pour la forme suivante la repeindrait.
L’instantané d’historique d’un drag est pris au premier mouvement, pas au
toucher : sélectionner sans bouger ne laisse aucune entrée vide.
backToSelect() abandonne le geste en cours (cancelGesture() : tracé ou pose retirés,
instantané rendu), désélectionne et passe à l’outil Sélection. Trois chemins y mènent :
un clic droit relâché à moins de 5 px de son appui (releasePointer), Échap, et le
bouton droit enfoncé pendant un geste du gauche — pointermove avec button === 2, car
un second bouton n’émet pas de pointerdown. Un clic droit sur une carte ne déplace
pas la vue : la caméra est remise où elle était. Le menu du navigateur est toujours
bloqué sur le tableau.
miz.js lit une .miz sans dépendance : répertoire central de l’archive zip, entrées
stockées ou décompressées par DecompressionStream('deflate-raw'), puis la table Lua
mission (et le dictionnaire l10n/DEFAULT) par un petit analyseur. Il en tire le
théâtre, les bullseyes, les vols pilotables et leurs points, les défenses aériennes
(reconnues au type DCS de leurs unités) et les navires. openMission() fait choisir le
vol s’il y en a plusieurs ; importMission() bâtit une nouvelle planche.
Depuis le lot 12, support liste les ravitailleurs et AWACS de l’IA (tâche du groupe
Refueling ou AWACS) : { name, side, role, type, points, orbit, tacan, freq }.
orbit vient de la première tâche Orbit de la route, déballée d’une ControlledTask
s’il le faut : { pattern, at, to, alt } (to, point suivant d’un Race-Track, ou
null), plus hot, len, width pour un Anchored. alt est celle de l’orbite, qui
gouverne en vol. tacan vient d’ActivateBeacon, freq (MHz) de SetFrequency, sinon
du groupe. importMission() en fait une route stroke tiretée, un symbole orbit et
l’appareil accroché à lui (hook, lot 10).
Les positions DCS sont en mètres, x vers le nord, y vers l’est. MIZ.toGeo() les porte
en latitude/longitude par la projection du théâtre, mesurée et non recopiée :
tools/build_projections.py ajuste une Mercator transverse WGS84 sur les balises de
l’installation du jeu (projections.js, écart de l’ordre de 4 cm). La projection et
son inverse (série de Krüger, Newton) sont testés contre pyproj par
node tools/test_miz.js. Sans projection mesurée, la planche est sans carte, nmPx
tiré des mètres, et prof.grid fait écrire les caps « G ».
dtcRoute() ramène les waypoints de la route (k: 'wp', variante m) aux mètres DCS :
sur une carte, MIZ.fromGeo() par la projection mesurée du théâtre ; sans carte, le
repère gardé à l’import, grid = { theatre, top, left, k, ox, oy } (x = top − (py − oy) / k,
y = left + (px − ox) / k). grid suit la planche dans « + phase » et dans le fichier
de briefing ; choisir ou ôter une carte l’efface, puisque les objets changent de repère.
L’altitude suit la règle de la route liée, en pieds, convertie en mètres.
MIZ.withDtc(bytes, ref, pts) écrit au format de l’éditeur de mission de DCS
(MissionEditor/modules/me_managerDTC.lua, CoreMods/aircraft/FA-18C/DTC) : une
cartouche est un fichier JSON DTC/<nom>.dtc de l’archive, et chaque unité la désigne
dans sa table DTC = { AutoLoad, Cartridges = { { name, default } } }. Rubrique écrite :
WYPT (NAV_PTS, séquence 1 dans NAV_ROUTE, mirror_NAV_PTS = false, théâtre) ; ETA
comme l’éditeur, heure de départ de la mission puis distance à 463 km/h. La mission
n’est pas réécrite : parseLua() garde la place de chaque valeur dans le texte
(SPAN), et seule la table DTC des F/A-18C du vol est remplacée ou insérée. Les autres
entrées de l’archive sont recopiées compressées, telles quelles. Vérifié par
node tools/test_miz.js.
present(on) bascule presenting, la classe present du corps de page (barres,
palette, onglets et contrôles de carte masqués) et le plein écran du navigateur. Tant
qu’on présente, pointerdown ne fait que le laser ou déplacer la carte, le clavier ne
répond qu’aux phases et à Échap, le double-clic est ignoré. Sortir du plein écran par
le navigateur (fullscreenchange) quitte aussi la présentation.
Animation entre phases. En présentation, la navigation passe par animateTo(i) :
la planche d’arrivée est chargée aussitôt, et anim retient les objets et la caméra de
la planche de départ. Pendant ANIM_MS, draw() remplace le temps d’un dessin objs
par tweenAt(k) et cam par la caméra interpolée, puis les rend : rien de transitoire
n’est jamais écrit. tweenAt() apparie les objets par uid (même type, même vue) et
interpole leurs nombres — sauf n —, leur cap par le plus court chemin, les points de
leurs tracés s’ils sont aussi nombreux, leur couleur ; un objet d’un seul côté reçoit
__alpha, que drawObj() applique. La route liée et la vue radar se recalculent sur
l’état interpolé. Entre deux repères différents, pas d’animation.
Le laser vit hors du tableau : laser.pts en pixels d’écran, chaque point daté, relié
au précédent s’il est du même glissé (s) ; laserTrail() ne rend que ceux de moins
de LASER_MS. drawLaser() le dessine en dernier et redemande un dessin tant qu’il
reste une trace : elle s’éteint d’elle-même. Ni objet, ni historique, ni export.
Le panneau du bas montre la coupe, ou l’écran radar d’un porteur : le symbole de
la vue de dessus marqué own: true, un seul par planche. Comme la route liée, rien
n’est stocké : radarView() recalcule l’image à chaque dessin avec RADAR.radarPicture
(radar.js, fonctions pures testées par node tools/test_radar.js).
air de la vue de dessus, porteur exclu.nmAt(y) sur une carte, nmPx sinon ; sans l’un ni l’autre, un message.fa18_tws, f16_tws), dessinée par drawObj ;
sa zone tactique, sa demi-largeur et son cône viennent de scope.area, scope.span
et scope.cone(az) — la même source que le kit radar.drawRadarCone() trace le volume balayé sur la vue de dessus, sous les objets.pointerdown et le double-clic y
rendent la main aussitôt.locked: true fige la géométrie et l’existence d’un objet, pas son style :
grab() l’ignore : avec une forme choisie, on pose par-dessus ;pick() lui préfère un objet libre au même point ; seul sous le doigt, il se
sélectionne, pour être libéré ;handleList() ne lui donne aucune poignée ; handles() dessine un cadre or et une
épingle, jamais exportés (les exports se dessinent sans sélection) ;Suppr, flèches du clavier et altitude tirée d’un waypoint le refusent, avec
un message (toast()) — jamais en silence ;snap() recopie tous les champs : l’historique et la sauvegarde gardent l’ancrage sans
code dédié.
Sur une carte, la position d’un symbole est un point du terrain, sa taille est fixe à
l’écran. Un objet posé sur un symbole doit donc vivre dans le repère du dessin de ce
symbole, pas sur le terrain : sinon le zoom l’en écarte (lot 10). hook le range ainsi :
u, v : sa place en tailles de symbole de l’hôte (SIZE × s), axes tournés de
a (0 pour une forme upright) ; da : son cap moins celui de l’hôte ;settle() place chaque objet accroché après son hôte, au zoom courant
(hookPlace()). Elle tourne au début de drawFrame() — donc aussi sur les copies
interpolées d’une transition —, dans topmost() (un zoom pas encore dessiné) et
autour du dessin de l’export kneeboard, qui a sa propre caméra ;placed (WeakMap) retient la position laissée par le dernier placement. Un objet qui
n’y est plus (glissé, tourné, recalé par snapAlt(), reprof(), convertPlan())
garde sa position : c’est son hook qui est recalculé (hookFrom()). Un objet
absent de placed — restauré par l’historique, relu d’un fichier — suit son hook ;hook est remplacé, jamais modifié en place : snap() copie superficiellement
et les instantanés de l’historique partagent sinon le même objet ;hooking, puis hookTo() au pointerdown suivant) ;
l’hôte est le symbole le plus haut sous le doigt, l’objet lui-même exclu ; une boucle
est refusée. hostOf() ne rend qu’un hôte présent dans la planche : un hôte effacé
laisse l’objet libre, l’historique le rend ;Ctrl+D recalcule son hook depuis sa place décalée ; handles() trace
le pointillé vers l’hôte ou vers les objets accrochés.dragm |
Déclenché par | Effet |
|---|---|---|
place |
pose d’un symbole, doigt maintenu | oriente et dimensionne dans le même geste ; en deçà de 14 px, garde les valeurs par défaut |
rot |
poignée d’un symbole | même calcul que place |
move |
outil sélection, ou outil symbole sur une forme existante | translation ; stroke translate tous ses points |
p1 / p2 |
poignées d’extrémité | déplace une extrémité ; recentre le contrôle si la flèche n’est pas courbée |
ctl |
poignée centrale | courbe la flèche et pose bent |
size |
poignée d’une image | échelle homothétique à partir de w0, h0 ; nmPx suit le même rapport |
v0, v1… |
sommets d’une zone | déplace ce sommet |
Trois formes du kit radar dérogent aux cas 5 et 6 : une vignette de marque (L&S,
DT2 du F/A-18C, cible désignée du F-16C) marque la piste touchée du même appareil, ou la
plus proche à 40 px près (markTrack(), une seule marque de chaque sorte par vue).
Ailleurs, elle désigne un écho : un écho brut (raw) est remplacé, à sa place et avec
son uid, par la piste newTrack de la marque ; sur le fond, cette piste se pose là où
l’on touche. Une piste ne porte qu’une marque : posée sur la piste qui porte l’autre,
la marque l’échange avec la piste qui la portait ; sans elle, une marque keep (la L&S)
n’est pas retirée, refus. Sur l’écran, la piste ou l’écho d’un autre appareil, elle
refuse ; un curseur (over) se pose par-dessus ce qu’il touche sans le
saisir ; un écran (under) reste traversable pour grab() et se glisse sous les
autres objets à la pose (insertLow()).
Une zone ne se crée pas par glissé : chaque pointerdown ajoute un sommet au
draft, le relâché ne termine rien. Elle se referme sur son premier point, par
double-clic ou Entrée ; Échap l’abandonne. Elle est insérée juste au-dessus des
images, donc sous les symboles qui la justifient.
hit() décide si un point touche un objet :
sym : disque de rayon SIZE × s × (SHAPES[k].hit ?? 1.15) — le rayon appartient
à la forme, pour qu’un bombardier s’attrape par l’aile et un SAM par son anneau ;arrow / line : échantillonnage de la courbe en 21 points, tolérance 12 px ;rect : couronne de 12 px autour du contour (l’intérieur ne prend pas) ;circle : proximité du cercle lui-même, pas du disque ;stroke : distance aux segments, tolérance 13 px — pas aux seuls points
enregistrés : un geste rapide n’en produit que quelques-uns et le milieu du trait
doit rester attrapable ;text, img : boîte englobante ;kframe : son bord, à 9 px près, et son titre — l’intérieur ne prend pas, on y
travaille sur le plan.pick() parcourt objs de la fin vers le début : ce qui est au-dessus se
désigne en premier.
s a deux sens. Échelle sur un sym, chaîne de caractères sur un text.
Aucun code générique ne les confond aujourd’hui, mais toute fonction qui
traiterait « n’importe quel objet » doit tester t d’abord.w n’est pas une taille. Sur un symbole, w reste l’épaisseur du trait ;
la taille est s. Les deux se règlent séparément dans la barre.snapshot() est
appelé avant la mutation ; si le tracé se révèle trop court, endPointer()
retire l’instantané par past.pop().restore() annule la sélection. Un objet sélectionné puis restauré par
annulation n’est plus le même objet en mémoire : garder une référence à travers
une annulation est faux.img. Sinon un clic dans une zone vide efface la carte de
fond. Une image se retire par sélection puis Suppr.ctx est réassigné pendant l’export kneeboard vers un canvas hors écran, et
tb (grossissement des textes) y passe au-dessus de 1. Toute fonction de dessin
doit lire ctx et tb au moment de l’appel, jamais les capturer. Le finally
de kneeboardCanvas() les remet en place, avec bare, qui retire les aides
d’édition (le cadre du kneeboard) de l’export ; l’export PNG le pose aussi.a = 0, alors
qu’une vue de dessus pointe vers le haut. paintSym, les poignées et le geste de
pose tiennent compte des deux conventions ; une silhouette qui vole vers la
gauche est retournée (scale(1, -1)), jamais tournée de 180°.reprof() ; modifier prof directement fausserait silencieusement
toutes les altitudes affichées.toScreen, w2s,
s2w ou nmAt — jamais comparer une coordonnée d’objet à celle du pointeur brut.pointerdown. Appuyé pendant qu’un
autre est tenu, il arrive en pointermove, button renseigné. Tout geste « au
bouton droit pendant… » se lit là.board.js?v=1.2). Modifier un
script sans monter son paramètre dans index.html laisse les navigateurs sur
l’ancienne version — le banc de saisie lui-même a d’abord jugé l’ancien code.past et future contiennent des instantanés complets : { objs, nmPx }, où
objs est copié objet par objet (pts dupliqué en profondeur, el partagé par
référence — une image n’est jamais dupliquée). Plafond : 100 entrées.
Ce choix coûte de la mémoire et évite une classe entière de bugs : annuler un effacement restitue ce qui a été effacé, ce qu’une pile de deltas rendait faux en v0.1 (voir ETAT.md §6).
| Clé | fl-briefing-board-v3 |
| Contenu | { cur, unit, split, showAF, headRef, boards: [{ name, nmPx, prof, map, cam, magDec, objs }] } — les objets écrits par record(), sans l’élément image ; l’historique n’est pas conservé |
| Images de fond | un objet img garde sa place, sa taille et son id ; ses octets vivent en IndexedDB (base fl-briefing-board, magasin images, clé = id). Au démarrage, restoreImages() les relit, retire les images introuvables en le disant, puis n’y garde que les id encore cités |
| Écriture | à chaque commit() |
| Échec | capturé et ignoré — le tableau reste utilisable, il ne se souvient pas |
La démo n’écrit ni en localStorage ni en IndexedDB.
✚ Nouveau (lot 11) remplace boards par une planche « Phase 1 » vide, vide
imgBlobs et imgEls, puis commit() réécrit la clé et idbKeep(new Set()) vide le
magasin d’images. Les préférences du poste (unit, split, showAF, headRef, dark)
restent. Comme openBriefing(), il ne laisse pas d’historique : il demande confirmation.
⇩ Briefing écrit un .json : { format: 'fl-briefing-board', version: 1, app, saved,
cur, unit, split, headRef, boards, images }, où boards suit l’écriture de la sauvegarde
locale (boardsRecord()) et images associe chaque id à sa data URL. openBriefing()
vérifie format et version, puis ne garde que ce que le moteur sait dessiner : types
d’objets connus, formes présentes dans SHAPES, théâtres connus, images en PNG, JPEG,
WebP ou GIF. Un fichier reçu vient d’ailleurs ; les noms de planches passent par
textContent, les étiquettes par le canvas.
La clé porte la version du format. v1 (jusqu’au 2026-09-17) ignorait a, s,
cx, cy : elle n’est pas relue, un ancien tableau est simplement oublié plutôt
que rechargé de travers. v2 (v0.2 à v0.4) est relue une fois, si v3 est
absente, et devient la planche « Phase 1 ».
Une entrée dans SHAPES suffit — la vignette, le groupe et l’outil suivent tout
seuls :
maClé: { g:'air', label:'Mon aéronef', s0:1.1, tile:.38, hit:1.25, draw(c){
sil(c, [[0,-1], …, [0,.9]]); // moitié droite, du nez à la queue, axe compris
body(c); // remplissage léger + contour
}},
| Champ | Rôle | Défaut |
|---|---|---|
g |
groupe : air, sol, tac |
requis |
label |
nom sous la vignette | requis |
draw(c) |
tracé dans le repère [-1, 1] |
requis |
s0 |
échelle à la pose | 1 |
tile |
échelle dans la vignette | .45 |
hit |
rayon de préhension | 1.15 |
num |
affiche un numéro au centre (waypoints) | absent |
tag |
étiquette fixe sous la forme | absent |
upright |
la forme ne tourne jamais ; seule sa tige suit a |
absent |
stem |
[début, fin] de la tige de cap, dans le repère [-1, 1] |
absent |
fixed |
le geste de pose oriente sans redimensionner ; la poignée redimensionne | absent |
under |
se pose sous les autres objets, traversable quand une forme est choisie | absent |
box |
désignation et encombrement carrés, demi-côté en unités du repère | absent |
smax |
échelle maximale au geste et à la poignée | 6 |
col |
couleur donnée à la pose, à la place de la couleur choisie | absent |
scope |
écran : page, rng, az, ranges, azs proposés au clavier, pb libellés des boutons |
absent |
track |
une piste : peut porter une marque de son groupe | absent |
tileA |
angle de la vignette, pour une forme qui se reconnaît inclinée | 0 |
mark |
la vignette marque la piste touchée au lieu de poser | absent |
newTrack |
d’une marque : la piste créée quand elle désigne un écho | absent |
keep |
d’une marque : jamais retirée par une autre marque posée sur sa piste | absent |
raw |
un écho brut : désigné, il devient la piste newTrack ; rawA, son cap alors |
absent |
over |
se pose par-dessus ce qu’on touche, sans le saisir | absent |
draw(c, o) reçoit aussi l’objet : un écran y lit son échelle et son azimut, un HAFU sa
marque. withMark() dessine la marque d’une piste ; paintSym() l’appelle en dernier,
par-dessus la tige de cap, et la marque porte un liseré de la couleur du fond d’écran
(HALO) qui la détache des traits du HAFU. Une forme droite sans tige garde une poignée, au coin haut droit, qui la
redimensionne. Le kit radar vit en fin de symbols.js, un groupe de palette par module,
et chaque élément cite sa source dans RADAR.md.
Aides disponibles : sil() (silhouette symétrique décrite d’un seul côté),
body() (remplissage léger + contour), pods() (nacelles symétriques).