@rr0/ufoathome 0.19.6 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -320,7 +320,7 @@ interface SightingRecordingJson {
320
320
  selected: boolean
321
321
  title?: string // shown as an on-canvas tooltip when hovered
322
322
  behindCloud?: boolean // the witness reported it behind cloud at this instant — stated, never deduced (see below)
323
- physical?: { sizeM: number, distanceM: number } // what the witness reported — see Apparent size
323
+ angular?: { widthDeg: number, heightDeg: number } // how big it LOOKEDthe only size a testimony holds, see Apparent size
324
324
  points?: { x: number, y: number }[] // "polygon" shapes only
325
325
  }
326
326
  }>
@@ -332,6 +332,7 @@ interface SightingRecordingJson {
332
332
  weatherTrack?: { keyframes: Array<{ t: number, weather: Weather }> }
333
333
  weather?: Weather // legacy static fallback for recordings predating weatherTrack
334
334
  weatherSource?: { id: string, name: string, url: string } // the meteorological record weatherTrack was looked up from — see Weather is looked up, not remembered. Absent = the witness's own account
335
+ instrument?: "eye" | "rectilinear-lens" // what it was observed THROUGH — see Instrument. Absent = the naked eye
335
336
  soundTrack?: { keyframes: Array<{ t: number, sound: { kind: "none" | "hum" | "whistle" | "rumble" | "crackle", volume: number, pitchHz: number, src?: string } }> } // what the witness heard — see What it sounded like
336
337
  decor?: DecorObject[] // buildings, trees, streetlights, vehicles, other witnesses — see src/engine/model/Decor.ts
337
338
  }
@@ -504,20 +505,80 @@ diff shows the weather and nothing else.
504
505
  appearance field, and held rather than blended. It is *stated*, for the same reason
505
506
  `DecorObject.occludesSourceIds` is: this format describes a 2D appearance on the witness's own field
506
507
  of view, not where an object was in space, so nothing in it can deduce whether cloud came between
507
- them. A reported distance (`physical`) is an occasional, often shaky extra, and the sky's own gaps
508
- are procedural noise — leaving the question to geometry means tuning the weather until the reported
509
- disappearance happens to occur. The witness's account outranks both; where a recording states a
510
- distance and makes no claim about cloud, the renderer falls back to the geometric test (is the deck
511
- crossed before the object, on the right side, with actual cloud in that direction).
512
-
513
- ### Apparent size
514
-
515
- `physical` is the witness's own reported size and distance, and the on-screen `bounds.width` is derived from it
516
- rather than drawn by eye: on the 640x360 canvas at the default 60° vertical field of view, one degree is about
517
- 5.4px and the full Moon about 2.8px, so an object of 3.5m at 90m is 12px wide — not the 90px an author reaches for
518
- unaided. `ApparentSize` (`src/engine/shape/ApparentSize.ts`) does the conversion both ways, and the editor's own
519
- **Real size** / **Distance** fields apply it and read back what the current drawing actually spans, in degrees and
520
- in full Moons.
508
+ them. A recording holds no distance at all (see *Apparent size* below), and the sky's own gaps are
509
+ procedural noise — leaving the question to geometry means tuning the weather until the reported
510
+ disappearance happens to occur. So the witness's account is the whole answer: no `behindCloud`, no
511
+ cloud.
512
+
513
+ There used to be a geometric fallback here, for a recording that stated a real distance and made no
514
+ claim about cloud. It went when stated distances did, and it had earned it: the one case it fired on
515
+ was Chiles-Whitted, where "it disappeared into the cloud deck" turned out to be an interrogator's
516
+ reconstruction that Whitted himself denied to McDonald in 1968.
517
+
518
+ ### Apparent size and why there is no real one
519
+
520
+ A witness never perceives meters. They perceive an angle: the thing covered a thumbnail at arm's length, or a fifth
521
+ of the windshield, or two full Moons. "About thirty meters long" is a conclusion they drew from a distance they
522
+ could not perceive either, and the two errors multiply. So a recording stores `angular` — how wide and how tall the
523
+ object *looked*, in degrees — and stores no real size and no real distance anywhere.
524
+
525
+ `bounds` is that angle projected onto the fixed 640x360 canvas at the pose's own field of view **and through the
526
+ recording's own instrument** (see below), which is what every editing gesture, hit-test and renderer keeps working
527
+ on. The angle is authoritative: `SightingShapes` (`src/engine/persistence/SightingShapes.ts`) re-derives `bounds`
528
+ from it on load and reads it back from `bounds` on save, so a file survives a change of canvas, of field of view or
529
+ of instrument, and if the two ever disagree the angle wins. `ImageProjection`
530
+ (`src/engine/instrument/ImageProjection.ts`) owns the conversion itself.
531
+
532
+ Through an eye at 60° across 360px, one degree is exactly 6px and the full Moon about 3.1px — so an object of 3.5m
533
+ at 90m is 13px wide, not the 90px an author reaches for unaided. That is why the editor's **Try a size** / **at a distance of**
534
+ fields exist: type a hypothesis, get the angle it implies on the canvas, and the meters are forgotten the moment
535
+ they have been applied. They are an authoring aid, never testimony.
536
+
537
+ ### Instrument — an eye is not a lens
538
+
539
+ `instrument` says what the observation was made through, and it changes the geometry of every frame.
540
+
541
+ A camera lens maps a direction to its sensor as `r = f·tan θ`: straight lines stay straight, and everything away
542
+ from the axis is stretched by `sec²θ` — 42% at 33° off-centre, 105% at the corner of a 16:9 frame with a 60°
543
+ vertical field. That is *correct* for a photograph and only looks right from the projection centre, about half an
544
+ image-width from the screen. An eye does no such thing: it perceives an angle as an angle wherever it falls, so
545
+ `instrument: "eye"` renders `r = f·θ` — image distance proportional to angle, which is what lets a ruler held to
546
+ the screen mean something. (A slight tangential stretch of `θ/sin θ` remains, 6% at 33°; no flat image escapes
547
+ trading one distortion for another.)
548
+
549
+ three.js's camera can only do the pinhole, so `EquidistantProjectionPass` renders the scene into an offscreen
550
+ target with a deliberately wider field and resamples it in one fullscreen pass. Everything that *aims* at the scene
551
+ rather than drawing it — the decor raycasts behind `isScreenPointOccluded` and `decorDistancesAt` — goes through
552
+ `directionFor`, since a point on the visible image no longer means what the pinhole camera thinks it means.
553
+
554
+ This is also why a change of instrument **moves** shapes and not just resizes them (`SightingShapes.reproject`): a
555
+ pixel only names a direction once a projection is named. Leaving positions alone is exactly how an object drawn in
556
+ several parts comes apart — the fuselage grows and its row of windows stays put.
557
+
558
+ Every case file here declares `eye`, because every one of them was watched rather than filmed. Until this existed
559
+ they were all rendered as photographs, which is what made Socorro's dynamite shack read as twice the size it
560
+ subtends.
561
+
562
+ One residual worth naming: an angular extent is stored as its *on-axis* value, and applied to a shape wherever it
563
+ sits. For an object 9° off-axis subtending 9°, that is about 2.5% out. The overlay draws axis-aligned boxes and
564
+ cannot express more; it is a fifth of the error it replaces, and it shrinks towards the centre of the frame.
565
+
566
+ #### Where meters do come back
567
+
568
+ The only real distance a testimony can support is an **inequality**, and only where the witness saw the object
569
+ cross something whose position is known: it passed *behind* that hangar (at least that far), or *in front of* that
570
+ tree (at most that far). `DecorObject.eastM/northM` give the decor its real position, `occludesSourceIds` says
571
+ which side of it the object was on, and `SceneRenderer.decorDistancesAt` raycasts the exact line of sight to
572
+ measure the crossing.
573
+
574
+ An angle plus a distance is a size, and a size does not change as the object flies — so every crossing narrows the
575
+ object's real width from one side, for the whole recording, and the narrowed width reads back as a distance at
576
+ every other instant. `SizeEstimate` (`src/engine/shape/SizeEstimate.ts`) accumulates that, reports a contradiction
577
+ rather than clamping one, and the editor prints the result under the apparent size.
578
+
579
+ Most sightings constrain nothing at all — a light in an empty night sky crosses nothing — and the readout then says
580
+ so. "Unknown" is the honest answer for a majority of cases, and saying it out loud is the entire point of not
581
+ storing a number instead.
521
582
 
522
583
  This format is deliberately independent of [`@rr0/data`](https://github.com/RR0/data)'s `RR0Event`/`@rr0/time`'s
523
584
  `Level2Date`/`@rr0/place`'s `Place` classes, even though its `time`/`place` fields are structurally aligned with
@@ -0,0 +1 @@
1
+ const e={oval:"Ovale",polygon:"Polygone",addVertex:"Ajouter un sommet",deleteVertex:"Supprimer le sommet",notAPolygon:"Sélectionnez une seule forme polygonale",tooFewVertices:"Une forme a besoin d'au moins 3 points",color:"Couleur",transparency:"Transparence",halo:"Halo",shape:"Forme",shapeTitle:"Nom",objectSize:"Essayer une taille",objectDistance:"à une distance de",apparentSize:"soit {deg}° — {moons}× la Lune",utcOffset:"Fuseau horaire",utcOffsetPlaceholder:"d'après la longitude",objectSizePlaceholder:"largeur supposée",objectDistancePlaceholder:"distance supposée",realSizeBetween:"Largeur réelle : entre {min} et {max} m, d'après ce qu'il croise",realSizeAtLeast:"Largeur réelle : au moins {min} m, d'après ce qu'il passe derrière",realSizeAtMost:"Largeur réelle : au plus {max} m, d'après ce qu'il passe devant",realSizeUnknown:"Largeur réelle indéterminée : rien dans la scène ne croise sa ligne de visée",realSizeContradiction:"Contradiction : les passages déclarés ne peuvent pas être vrais ensemble",realDistanceHere:" — soit ici entre {min} et {max} m",addShape:"Ajouter une forme",deleteShape:"Supprimer la forme",play:"Lecture",pause:"Pause",noDuration:"Aucune durée d'observation",autoReplay:"Lecture automatique",group:"Grouper",ungroup:"Dégrouper",bringToFront:"Placer devant",sendToBack:"Placer derrière",contextMenuDelete:"Supprimer",confirmDeleteShape:"Supprimer {name} ? Cette action est irréversible.",confirmDeleteShapes:"Supprimer {count} formes ? Cette action est irréversible.",onlyOneShape:"Il n'y a qu'une seule forme",alreadyAtFront:"Cette forme est déjà la plus en avant",alreadyAtBack:"Cette forme est déjà la plus en arrière",needTwoShapesToGroup:"Sélectionnez au moins deux formes pour les grouper",notGrouped:"Cette forme ne fait pas partie d'un groupe",multipleShapesSelected:"Plusieurs formes sélectionnées — sélectionnez-en une seule pour modifier ceci",cloudBase:"Base des nuages",elevation:"Altitude",samplingRate:"Échantillonnage",duration:"Durée",durationPlaceholder:"durée de l'observation",durationImprecise:"Ces dates ne permettent pas de déterminer précisément une durée — saisissez-la manuellement.",export:"Exporter le JSON",importFile:"Charger un fichier JSON",importUrl:"Ou charger depuis une URL",importUrlPlaceholder:"https://…/sighting.json",importButton:"Charger",importError:"Impossible de charger cet enregistrement — vérifiez le fichier ou l'URL et réessayez.",record:"Enregistrer",stop:"Arrêter",according:"selon",sourceElevation:"Relief",sourceImagery:"Imagerie",placeName:"Lieu",placeNamePlaceholder:"Valensole, France",searchPlace:"Localiser",placeMatch:"Correspondances",placeSearching:"Recherche du lieu…",placeMatchFound:"lieu trouvé",placeMatchesFound:"lieux trouvés",placeNotFound:"Aucun lieu de ce nom",placeSearchFailed:"Recherche de lieu momentanément indisponible",utcOffsetImplausible:"Ce fuseau ne correspond pas à la longitude saisie, dont l'heure solaire est UTC{solar} — la scène et le relevé météo obéissent pourtant à ce qui est déclaré ici. Videz le champ pour le déduire de la longitude.",timeZoneManual:"décalage saisi",altitudeAboveSeaLevel:"Altitude du témoin au-dessus du niveau de la mer — le sol du lieu saisi en fixe le minimum : un témoin dans les Alpes n'est pas à 0 m.",groundAt:"sol à {m} m",latitude:"Latitude",longitude:"Longitude",heading:"Orientation",headingPlaceholder:"inconnu",pitch:"Inclinaison",observationTime:"Début de l'observation",observationEndTime:"Fin de l'observation",edtfInvalid:"Date/heure EDTF invalide (ex. 1965-07-01T05:00, 2025-06?, 2025~).",edtfPlaceholder:"AAAA-MM-JJThh:mm[?~%] ou hh:mm",observationTimeHint:"EDTF — ex. 1965-07-01T05:00, 2025-06? (incertain), 2025~ (approximatif), ou juste 05:00 si la date est inconnue",observationEndTimeHint:"EDTF — ex. 1965-07-01T05:10, 2025-06? (incertain), 2025~ (approximatif), ou juste 05:10 si la date est inconnue",presetsGroupLabel:"Forme de l'ovni",witnessId:"ID témoin",witnessDirName:"Répertoire du témoin",witnessTitle:"Titre du témoin",witnessLastName:"Nom de famille du témoin",witnessFirstNames:"Prénoms du témoin",caseId:"ID de l'affaire",description:"Description",tags:"Mots-clés",tagsPlaceholder:"séparés par des virgules",weather:"Météo",shapeGroup:"Forme",soundGroup:"Son",temporalGroup:"Date et heure",locationGroup:"Lieu",observationGroup:"Observation",witnessGroup:"Témoin",circumstancesGroup:"Circonstances",weatherInferred:"D'après les relevés",weatherInferredTitle:"La météo n'est pas un témoignage : c'est un fait mesurable, relevé pour la date, l'heure et le lieu de l'observation. Décochez pour saisir les conditions rapportées par le témoin, qui priment alors sur le relevé.",weatherLookingUp:"Recherche du relevé…",weatherNeedsDateAndPlace:"Date complète et lieu requis pour retrouver le relevé",weatherNoRecord:"Aucun relevé pour cette date et ce lieu",weatherLookupFailed:"Relevés météo momentanément inaccessibles",cloudCover:"Couverture nuageuse",cloudDarkness:"Obscurité des nuages",precipitationType:"Précipitations",precipitationNone:"Aucune",precipitationRain:"Pluie",precipitationSnow:"Neige",precipitationHail:"Grêle",precipitationIntensity:"Intensité",windDirection:"Direction du vent",windSpeed:"Force du vent",storm:"Orage",soundKind:"Son",soundNone:"Aucun (silencieux)",soundHum:"Bourdonnement",soundWhistle:"Sifflement",soundRumble:"Grondement",soundCrackle:"Crépitement",soundVolume:"Intensité sonore",soundPitch:"Hauteur",soundSrc:"Enregistrement",soundSrcPlaceholder:"URL d'un enregistrement réel",lensFlareBrightness:"Intensité lumineuse",instrument:"Observé avec",cameraDevice:"Appareil photo/vidéo",decor:"Décor",decorBuilding:"Bâtiment",decorTree:"Arbre",decorStreetlight:"Lampadaire",decorVehicle:"Véhicule",decorWitness:"Autre témoin",addDecor:"Ajouter",deleteDecor:"Supprimer l'élément de décor",decorEast:"Distance à l'est",decorNorth:"Distance au nord",decorHeading:"Orientation",decorLit:"Allumé",decorTitle:"Nom",decorSightingUrl:"URL de l'enregistrement du témoin",viewTestimony:"Voir le témoignage",noWitnessRecording:"Aucune URL d'enregistrement définie pour ce témoin",masks:"Masque",addWitness:"Ajouter un témoin",decorFloors:"Étages",decorOccupiedFloor:"Étage occupé",decorWitnessSide:"Emplacement du témoin",decorWitnessSideNone:"Non présent",decorWindows:"Fenêtres",decorSideFront:"Avant",decorSideBehind:"Arrière",decorSideLeft:"Gauche",decorSideRight:"Droite",decorSideFrontLeft:"Avant-gauche",decorSideFrontRight:"Avant-droit",decorSideBehindLeft:"Arrière-gauche",decorSideBehindRight:"Arrière-droit"};export{e as ufoRecorderMessages_fr};