FL Briefing Board

Modèle d’état — FL Briefing Board

Contrat interne du moteur, à jour au 2026-09-30 (v1.3). À lire avant toute évolution de board.js ou symbols.js. L’état d’avancement du projet est dans ETAT.md.

1. Vue d’ensemble

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.

Deux vues, un seul moteur

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.

Carte vivante

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 :

Caps vrais et magnétiques

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.

Vitrine LK Studio et démo

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.

Route liée

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.

2. La scène — 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.

Géométrie d’un symbole

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.

Géométrie d’une flèche

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.

3. L’état d’entré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

Distances et caps

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.

4. La machine d’interaction

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.

Retour à la sélection

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.

Import d’une mission

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 ».

Route dans la DTC

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.

Mode présentation

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.

Vue radar liée

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).

Objets ancrés

locked: true fige la géométrie et l’existence d’un objet, pas son style :

snap() recopie tous les champs : l’historique et la sauvegarde gardent l’ancrage sans code dédié.

Objets accrochés

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 :

Modes de drag

m 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.

Désignation

hit() décide si un point touche un objet :

pick() parcourt objs de la fin vers le début : ce qui est au-dessus se désigne en premier.

5. Pièges à connaître

  1. 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.
  2. 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.
  3. Un geste avorté ne doit pas laisser d’entrée d’historique. snapshot() est appelé avant la mutation ; si le tracé se révèle trop court, endPointer() retire l’instantané par past.pop().
  4. 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.
  5. La gomme ignore les img. Sinon un clic dans une zone vide efface la carte de fond. Une image se retire par sélection puis Suppr.
  6. 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.
  7. Dans la coupe, une silhouette de profil pointe à droite pour 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°.
  8. Une altitude est une position écran. Tout ce qui change le repère de la coupe doit passer par reprof() ; modifier prof directement fausserait silencieusement toutes les altitudes affichées.
  9. Un objet du plan n’est pas en pixels écran quand une carte est chargée. Toute nouvelle fonction qui mesure, désigne ou place doit passer par toScreen, w2s, s2w ou nmAt — jamais comparer une coordonnée d’objet à celle du pointeur brut.
  10. Un second bouton de souris n’émet pas de pointerdown. Appuyé pendant qu’un autre est tenu, il arrive en pointermove, button renseigné. Tout geste « au bouton droit pendant… » se lit là.
  11. Les scripts sont chargés à une adresse versionnée (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.

6. Historique

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).

7. Persistance

   
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.

Fichier de briefing

⇩ 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 ».

8. Ajouter une forme

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).