@the-inclusionist/engine 10.0.0 → 11.0.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.
Files changed (528) hide show
  1. package/README.md +32 -1
  2. package/app/css/style.css +216 -42
  3. package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
  4. package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
  5. package/app/public/vendor/fonts.css +11 -1175
  6. package/dist-pkg/boot/create-game.d.ts +84 -33
  7. package/dist-pkg/boot/create-game.js +708 -307
  8. package/dist-pkg/core/a11y-sr.d.ts +1 -1
  9. package/dist-pkg/core/a11y-sr.js +3 -5
  10. package/dist-pkg/core/accommodations.d.ts +26 -8
  11. package/dist-pkg/core/accommodations.js +27 -10
  12. package/dist-pkg/core/actions.d.ts +39 -10
  13. package/dist-pkg/core/actions.js +28 -6
  14. package/dist-pkg/core/contract.d.ts +0 -2
  15. package/dist-pkg/core/contract.js +0 -2
  16. package/dist-pkg/core/i18n.d.ts +36 -71
  17. package/dist-pkg/core/i18n.js +131 -198
  18. package/dist-pkg/core/session-clock.d.ts +23 -0
  19. package/dist-pkg/core/session-clock.js +37 -0
  20. package/dist-pkg/i18n/en.js +44 -49
  21. package/dist-pkg/i18n/es.js +44 -49
  22. package/dist-pkg/i18n/pt.js +49 -54
  23. package/dist-pkg/input/gamepad.d.ts +42 -11
  24. package/dist-pkg/input/gamepad.js +50 -15
  25. package/dist-pkg/input/key-default.d.ts +30 -0
  26. package/dist-pkg/input/key-default.js +60 -0
  27. package/dist-pkg/input/keyboard-runtime.d.ts +0 -4
  28. package/dist-pkg/input/keyboard.d.ts +0 -1
  29. package/dist-pkg/input/keydown.d.ts +5 -11
  30. package/dist-pkg/input/keydown.js +3 -3
  31. package/dist-pkg/input/latch-edge.d.ts +10 -2
  32. package/dist-pkg/input/latch-edge.js +4 -3
  33. package/dist-pkg/input/latch-scope.d.ts +27 -22
  34. package/dist-pkg/input/latch-scope.js +25 -37
  35. package/dist-pkg/input/latch-store.d.ts +9 -12
  36. package/dist-pkg/input/latch-store.js +13 -18
  37. package/dist-pkg/input/latch-sync.d.ts +5 -4
  38. package/dist-pkg/input/latch-sync.js +5 -5
  39. package/dist-pkg/input/pad-reading.js +7 -2
  40. package/dist-pkg/input/pad-wizard.d.ts +4 -1
  41. package/dist-pkg/input/pad-wizard.js +60 -9
  42. package/dist-pkg/input/switch-scan.d.ts +29 -12
  43. package/dist-pkg/input/switch-scan.js +15 -2
  44. package/dist-pkg/input/touch-bindings.d.ts +3 -6
  45. package/dist-pkg/input/touch.d.ts +17 -2
  46. package/dist-pkg/input/touch.js +55 -17
  47. package/dist-pkg/input/transport-in-use.d.ts +5 -5
  48. package/dist-pkg/input/transport-in-use.js +5 -5
  49. package/dist-pkg/input/transports.d.ts +0 -9
  50. package/dist-pkg/input/transports.js +1 -1
  51. package/dist-pkg/input/virtual-controller.d.ts +39 -3
  52. package/dist-pkg/input/virtual-controller.js +32 -4
  53. package/dist-pkg/input/voice-map.d.ts +27 -11
  54. package/dist-pkg/input/voice-map.js +41 -43
  55. package/dist-pkg/platform/audio-ambient.d.ts +0 -1
  56. package/dist-pkg/platform/audio-ambient.js +3 -1
  57. package/dist-pkg/platform/audio-mixer.js +4 -9
  58. package/dist-pkg/platform/audio-sonar.d.ts +8 -94
  59. package/dist-pkg/platform/audio-sonar.js +22 -207
  60. package/dist-pkg/platform/audio.d.ts +10 -1
  61. package/dist-pkg/platform/audio.js +35 -2
  62. package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
  63. package/dist-pkg/platform/choose-by-voice.js +157 -0
  64. package/dist-pkg/platform/font-library.d.ts +77 -0
  65. package/dist-pkg/platform/font-library.js +98 -0
  66. package/dist-pkg/platform/font-library.json +721 -0
  67. package/dist-pkg/platform/heavy-catalogue.d.ts +46 -0
  68. package/dist-pkg/platform/heavy-catalogue.js +47 -14
  69. package/dist-pkg/platform/heavy-mirror.d.ts +3 -9
  70. package/dist-pkg/platform/heavy-mirror.js +14 -17
  71. package/dist-pkg/platform/heavy.d.ts +40 -8
  72. package/dist-pkg/platform/heavy.js +177 -22
  73. package/dist-pkg/platform/interruptible-speech.d.ts +15 -8
  74. package/dist-pkg/platform/interruptible-speech.js +37 -13
  75. package/dist-pkg/platform/listener-scope.js +22 -13
  76. package/dist-pkg/platform/locale-host.d.ts +7 -3
  77. package/dist-pkg/platform/locale-host.js +13 -2
  78. package/dist-pkg/platform/reading-in-worker.d.ts +24 -0
  79. package/dist-pkg/platform/reading-in-worker.js +38 -0
  80. package/dist-pkg/platform/reading-model.d.ts +21 -0
  81. package/dist-pkg/platform/reading-model.js +25 -0
  82. package/dist-pkg/platform/reading-runtime.js +13 -8
  83. package/dist-pkg/platform/reading.d.ts +12 -1
  84. package/dist-pkg/platform/reading.js +9 -1
  85. package/dist-pkg/platform/speech-recognition.d.ts +10 -1
  86. package/dist-pkg/platform/speech-recognition.js +29 -9
  87. package/dist-pkg/platform/speech.d.ts +10 -1
  88. package/dist-pkg/platform/speech.js +24 -13
  89. package/dist-pkg/platform/tts.d.ts +37 -4
  90. package/dist-pkg/platform/tts.js +279 -57
  91. package/dist-pkg/platform/voice-listener.d.ts +6 -2
  92. package/dist-pkg/platform/voice-listener.js +1 -1
  93. package/dist-pkg/platform/voice-plan.d.ts +13 -0
  94. package/dist-pkg/platform/voice-plan.js +57 -2
  95. package/dist-pkg/platform/vosk-runtime.d.ts +25 -0
  96. package/dist-pkg/platform/vosk-runtime.js +59 -1
  97. package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
  98. package/dist-pkg/platform/vosk-vocabulary.js +178 -0
  99. package/dist-pkg/render/crt.d.ts +11 -9
  100. package/dist-pkg/render/crt.js +6 -10
  101. package/dist-pkg/render/cvd-matrices.d.ts +5 -1
  102. package/dist-pkg/render/cvd-matrices.js +5 -1
  103. package/dist-pkg/render/high-contrast.d.ts +1 -2
  104. package/dist-pkg/render/high-contrast.js +5 -1
  105. package/dist-pkg/render/viz-axes.d.ts +9 -0
  106. package/dist-pkg/render/viz-axes.js +15 -2
  107. package/dist-pkg/ui/aac-sets.d.ts +29 -24
  108. package/dist-pkg/ui/aac-sets.js +22 -30
  109. package/dist-pkg/ui/audio-choices.d.ts +10 -5
  110. package/dist-pkg/ui/audio-choices.js +14 -10
  111. package/dist-pkg/ui/camera-control.d.ts +47 -0
  112. package/dist-pkg/ui/camera-control.js +35 -1
  113. package/dist-pkg/ui/declared-words.d.ts +21 -0
  114. package/dist-pkg/ui/declared-words.js +66 -0
  115. package/dist-pkg/ui/drawing-problems.js +4 -3
  116. package/dist-pkg/ui/eye-control.js +17 -17
  117. package/dist-pkg/ui/face-control.js +15 -15
  118. package/dist-pkg/ui/fonts.d.ts +42 -8
  119. package/dist-pkg/ui/fonts.js +58 -24
  120. package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
  121. package/dist-pkg/ui/footer-scroll-driver.js +201 -0
  122. package/dist-pkg/ui/footer-scroll.d.ts +38 -0
  123. package/dist-pkg/ui/footer-scroll.js +55 -0
  124. package/dist-pkg/ui/game-options.d.ts +14 -9
  125. package/dist-pkg/ui/game-options.js +41 -21
  126. package/dist-pkg/ui/hand-control.js +15 -15
  127. package/dist-pkg/ui/help-panel.d.ts +19 -8
  128. package/dist-pkg/ui/help-panel.js +15 -5
  129. package/dist-pkg/ui/hud-bands.d.ts +29 -12
  130. package/dist-pkg/ui/hud-bands.js +63 -25
  131. package/dist-pkg/ui/hud-row.d.ts +34 -0
  132. package/dist-pkg/ui/hud-row.js +80 -0
  133. package/dist-pkg/ui/hud.d.ts +0 -3
  134. package/dist-pkg/ui/layout.js +5 -2
  135. package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
  136. package/dist-pkg/ui/libras-avatar-clip.js +86 -0
  137. package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
  138. package/dist-pkg/ui/libras-avatar-load.js +134 -0
  139. package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
  140. package/dist-pkg/ui/libras-avatar-plan.js +211 -0
  141. package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
  142. package/dist-pkg/ui/libras-avatar-player.js +226 -0
  143. package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
  144. package/dist-pkg/ui/libras-avatar-stage.js +208 -0
  145. package/dist-pkg/ui/libras-glosses.d.ts +28 -0
  146. package/dist-pkg/ui/libras-glosses.js +161 -0
  147. package/dist-pkg/ui/menu-intent.d.ts +29 -0
  148. package/dist-pkg/ui/menu-intent.js +35 -1
  149. package/dist-pkg/ui/menu-items.d.ts +10 -0
  150. package/dist-pkg/ui/menu-items.js +29 -0
  151. package/dist-pkg/ui/menu-nav.d.ts +40 -4
  152. package/dist-pkg/ui/menu-nav.js +132 -47
  153. package/dist-pkg/ui/panel-widgets.d.ts +34 -0
  154. package/dist-pkg/ui/panel-widgets.js +86 -0
  155. package/dist-pkg/ui/pause-icons.d.ts +30 -14
  156. package/dist-pkg/ui/pause-icons.js +106 -45
  157. package/dist-pkg/ui/scan-overlay.d.ts +3 -3
  158. package/dist-pkg/ui/scan-overlay.js +11 -3
  159. package/dist-pkg/ui/screen-text.d.ts +25 -0
  160. package/dist-pkg/ui/screen-text.js +152 -0
  161. package/dist-pkg/ui/session-clock.d.ts +32 -0
  162. package/dist-pkg/ui/session-clock.js +67 -0
  163. package/dist-pkg/ui/settings-aac.d.ts +3 -1
  164. package/dist-pkg/ui/settings-aac.js +32 -13
  165. package/dist-pkg/ui/settings-audio.d.ts +10 -5
  166. package/dist-pkg/ui/settings-audio.js +101 -35
  167. package/dist-pkg/ui/settings-controls.d.ts +0 -4
  168. package/dist-pkg/ui/settings-mobility.d.ts +3 -6
  169. package/dist-pkg/ui/settings-mobility.js +4 -46
  170. package/dist-pkg/ui/settings-motion.js +2 -2
  171. package/dist-pkg/ui/settings-panel.d.ts +10 -0
  172. package/dist-pkg/ui/settings-panel.js +25 -1
  173. package/dist-pkg/ui/settings-typo.d.ts +5 -6
  174. package/dist-pkg/ui/settings-typo.js +20 -11
  175. package/dist-pkg/ui/settings-visual.js +2 -2
  176. package/dist-pkg/ui/shell.d.ts +1 -3
  177. package/dist-pkg/ui/shell.js +8 -4
  178. package/dist-pkg/ui/title.d.ts +2 -11
  179. package/dist-pkg/ui/title.js +39 -6
  180. package/dist-pkg/ui/top-band.d.ts +5 -3
  181. package/dist-pkg/ui/top-band.js +21 -25
  182. package/dist-pkg/ui/typo-choices.d.ts +3 -3
  183. package/dist-pkg/ui/typo-choices.js +9 -8
  184. package/dist-pkg/ui/vlibras.d.ts +71 -27
  185. package/dist-pkg/ui/vlibras.js +116 -44
  186. package/dist-pkg/ui/voice-control.d.ts +23 -1
  187. package/dist-pkg/ui/voice-control.js +149 -16
  188. package/dist-pkg/ui/voice-settings.d.ts +2 -3
  189. package/dist-pkg/ui/voice-settings.js +53 -3
  190. package/dist-pkg/ui/where-the-child-is.js +2 -2
  191. package/docs/CREDITS.md +122 -10
  192. package/docs/LICENSES.md +36 -7
  193. package/package.json +34 -9
  194. package/scripts/check-cartridge.mjs +78 -0
  195. package/scripts/game-build.d.mts +25 -0
  196. package/scripts/game-build.mjs +144 -0
  197. package/scripts/heavy-into-the-delivery.mjs +284 -40
  198. package/scripts/libras-avatar.json +680 -0
  199. package/scripts/libras-avatar.mjs +246 -0
  200. package/scripts/libras-glosses/gloss.py +95 -0
  201. package/scripts/libras-glosses/pyproject.toml +32 -0
  202. package/scripts/libras-glosses/uv.lock +781 -0
  203. package/scripts/libras-glosses.mjs +351 -0
  204. package/scripts/licences/OFL-1.1.txt +86 -0
  205. package/scripts/licences/UFL-1.0.txt +96 -0
  206. package/scripts/licences/fonts.mjs +389 -0
  207. package/scripts/licences/third-party.mjs +35 -21
  208. package/app/public/vendor/fonts/bitter-var-ext.woff2 +0 -0
  209. package/app/public/vendor/fonts/bitter-var.woff2 +0 -0
  210. package/app/public/vendor/fonts/bodonimoda-var-ext.woff2 +0 -0
  211. package/app/public/vendor/fonts/bodonimoda-var.woff2 +0 -0
  212. package/app/public/vendor/fonts/cat-abril_fatface-400-ext.woff2 +0 -0
  213. package/app/public/vendor/fonts/cat-abril_fatface-400.woff2 +0 -0
  214. package/app/public/vendor/fonts/cat-adlam_display-400-ext.woff2 +0 -0
  215. package/app/public/vendor/fonts/cat-adlam_display-400.woff2 +0 -0
  216. package/app/public/vendor/fonts/cat-agu_display-400-ext.woff2 +0 -0
  217. package/app/public/vendor/fonts/cat-agu_display-400.woff2 +0 -0
  218. package/app/public/vendor/fonts/cat-alfa_slab_one-400-ext.woff2 +0 -0
  219. package/app/public/vendor/fonts/cat-alfa_slab_one-400.woff2 +0 -0
  220. package/app/public/vendor/fonts/cat-are_you_serious-400-ext.woff2 +0 -0
  221. package/app/public/vendor/fonts/cat-are_you_serious-400.woff2 +0 -0
  222. package/app/public/vendor/fonts/cat-asimovian-400-ext.woff2 +0 -0
  223. package/app/public/vendor/fonts/cat-asimovian-400.woff2 +0 -0
  224. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400-ext.woff2 +0 -0
  225. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400.woff2 +0 -0
  226. package/app/public/vendor/fonts/cat-bakbak_one-400-ext.woff2 +0 -0
  227. package/app/public/vendor/fonts/cat-bakbak_one-400.woff2 +0 -0
  228. package/app/public/vendor/fonts/cat-bangers-400-ext.woff2 +0 -0
  229. package/app/public/vendor/fonts/cat-bangers-400.woff2 +0 -0
  230. package/app/public/vendor/fonts/cat-barriecito-400-ext.woff2 +0 -0
  231. package/app/public/vendor/fonts/cat-barriecito-400.woff2 +0 -0
  232. package/app/public/vendor/fonts/cat-berkshire_swash-400-ext.woff2 +0 -0
  233. package/app/public/vendor/fonts/cat-berkshire_swash-400.woff2 +0 -0
  234. package/app/public/vendor/fonts/cat-bevan-400-ext.woff2 +0 -0
  235. package/app/public/vendor/fonts/cat-bevan-400.woff2 +0 -0
  236. package/app/public/vendor/fonts/cat-bitcount-400-ext.woff2 +0 -0
  237. package/app/public/vendor/fonts/cat-bitcount-400.woff2 +0 -0
  238. package/app/public/vendor/fonts/cat-bowlby_one_sc-400-ext.woff2 +0 -0
  239. package/app/public/vendor/fonts/cat-bowlby_one_sc-400.woff2 +0 -0
  240. package/app/public/vendor/fonts/cat-bungee_shade-400-ext.woff2 +0 -0
  241. package/app/public/vendor/fonts/cat-bungee_shade-400.woff2 +0 -0
  242. package/app/public/vendor/fonts/cat-charm-400-ext.woff2 +0 -0
  243. package/app/public/vendor/fonts/cat-charm-400.woff2 +0 -0
  244. package/app/public/vendor/fonts/cat-charm-700-ext.woff2 +0 -0
  245. package/app/public/vendor/fonts/cat-charm-700.woff2 +0 -0
  246. package/app/public/vendor/fonts/cat-cherry_bomb_one-400-ext.woff2 +0 -0
  247. package/app/public/vendor/fonts/cat-cherry_bomb_one-400.woff2 +0 -0
  248. package/app/public/vendor/fonts/cat-chokokutai-400-ext.woff2 +0 -0
  249. package/app/public/vendor/fonts/cat-chokokutai-400.woff2 +0 -0
  250. package/app/public/vendor/fonts/cat-chonburi-400-ext.woff2 +0 -0
  251. package/app/public/vendor/fonts/cat-chonburi-400.woff2 +0 -0
  252. package/app/public/vendor/fonts/cat-climate_crisis-400-ext.woff2 +0 -0
  253. package/app/public/vendor/fonts/cat-climate_crisis-400.woff2 +0 -0
  254. package/app/public/vendor/fonts/cat-cookie-400.woff2 +0 -0
  255. package/app/public/vendor/fonts/cat-dela_gothic_one-400-ext.woff2 +0 -0
  256. package/app/public/vendor/fonts/cat-dela_gothic_one-400.woff2 +0 -0
  257. package/app/public/vendor/fonts/cat-fascinate_inline-400-ext.woff2 +0 -0
  258. package/app/public/vendor/fonts/cat-fascinate_inline-400.woff2 +0 -0
  259. package/app/public/vendor/fonts/cat-ga_maamli-400-ext.woff2 +0 -0
  260. package/app/public/vendor/fonts/cat-ga_maamli-400.woff2 +0 -0
  261. package/app/public/vendor/fonts/cat-itim-400-ext.woff2 +0 -0
  262. package/app/public/vendor/fonts/cat-itim-400.woff2 +0 -0
  263. package/app/public/vendor/fonts/cat-jacquard_24-400-ext.woff2 +0 -0
  264. package/app/public/vendor/fonts/cat-jacquard_24-400.woff2 +0 -0
  265. package/app/public/vendor/fonts/cat-kablammo-400-ext.woff2 +0 -0
  266. package/app/public/vendor/fonts/cat-kablammo-400.woff2 +0 -0
  267. package/app/public/vendor/fonts/cat-kalam-400-ext.woff2 +0 -0
  268. package/app/public/vendor/fonts/cat-kalam-400.woff2 +0 -0
  269. package/app/public/vendor/fonts/cat-kalam-700-ext.woff2 +0 -0
  270. package/app/public/vendor/fonts/cat-kalam-700.woff2 +0 -0
  271. package/app/public/vendor/fonts/cat-katibeh-400-ext.woff2 +0 -0
  272. package/app/public/vendor/fonts/cat-katibeh-400.woff2 +0 -0
  273. package/app/public/vendor/fonts/cat-lemonada-400-ext.woff2 +0 -0
  274. package/app/public/vendor/fonts/cat-lemonada-400.woff2 +0 -0
  275. package/app/public/vendor/fonts/cat-lilita_one-400-ext.woff2 +0 -0
  276. package/app/public/vendor/fonts/cat-lilita_one-400.woff2 +0 -0
  277. package/app/public/vendor/fonts/cat-lobster-400-ext.woff2 +0 -0
  278. package/app/public/vendor/fonts/cat-lobster-400.woff2 +0 -0
  279. package/app/public/vendor/fonts/cat-luckiest_guy-400-ext.woff2 +0 -0
  280. package/app/public/vendor/fonts/cat-luckiest_guy-400.woff2 +0 -0
  281. package/app/public/vendor/fonts/cat-marhey-400-ext.woff2 +0 -0
  282. package/app/public/vendor/fonts/cat-marhey-400.woff2 +0 -0
  283. package/app/public/vendor/fonts/cat-modak-400-ext.woff2 +0 -0
  284. package/app/public/vendor/fonts/cat-modak-400.woff2 +0 -0
  285. package/app/public/vendor/fonts/cat-momo_signature-400-ext.woff2 +0 -0
  286. package/app/public/vendor/fonts/cat-momo_signature-400.woff2 +0 -0
  287. package/app/public/vendor/fonts/cat-monofett-400-ext.woff2 +0 -0
  288. package/app/public/vendor/fonts/cat-monofett-400.woff2 +0 -0
  289. package/app/public/vendor/fonts/cat-monomakh-400-ext.woff2 +0 -0
  290. package/app/public/vendor/fonts/cat-monomakh-400.woff2 +0 -0
  291. package/app/public/vendor/fonts/cat-monoton-400-ext.woff2 +0 -0
  292. package/app/public/vendor/fonts/cat-monoton-400.woff2 +0 -0
  293. package/app/public/vendor/fonts/cat-nerko_one-400-ext.woff2 +0 -0
  294. package/app/public/vendor/fonts/cat-nerko_one-400.woff2 +0 -0
  295. package/app/public/vendor/fonts/cat-noto_sans_adlam-400-ext.woff2 +0 -0
  296. package/app/public/vendor/fonts/cat-noto_sans_adlam-400.woff2 +0 -0
  297. package/app/public/vendor/fonts/cat-noto_sans_arabic-400-ext.woff2 +0 -0
  298. package/app/public/vendor/fonts/cat-noto_sans_arabic-400.woff2 +0 -0
  299. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400-ext.woff2 +0 -0
  300. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400.woff2 +0 -0
  301. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400-ext.woff2 +0 -0
  302. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400.woff2 +0 -0
  303. package/app/public/vendor/fonts/cat-noto_sans_jp-400-ext.woff2 +0 -0
  304. package/app/public/vendor/fonts/cat-noto_sans_jp-400.woff2 +0 -0
  305. package/app/public/vendor/fonts/cat-noto_sans_khmer-400-ext.woff2 +0 -0
  306. package/app/public/vendor/fonts/cat-noto_sans_khmer-400.woff2 +0 -0
  307. package/app/public/vendor/fonts/cat-noto_sans_kr-400-ext.woff2 +0 -0
  308. package/app/public/vendor/fonts/cat-noto_sans_kr-400.woff2 +0 -0
  309. package/app/public/vendor/fonts/cat-noto_sans_sc-400-ext.woff2 +0 -0
  310. package/app/public/vendor/fonts/cat-noto_sans_sc-400.woff2 +0 -0
  311. package/app/public/vendor/fonts/cat-noto_sans_thai-400-ext.woff2 +0 -0
  312. package/app/public/vendor/fonts/cat-noto_sans_thai-400.woff2 +0 -0
  313. package/app/public/vendor/fonts/cat-ole-400-ext.woff2 +0 -0
  314. package/app/public/vendor/fonts/cat-ole-400.woff2 +0 -0
  315. package/app/public/vendor/fonts/cat-pacifico-400-ext.woff2 +0 -0
  316. package/app/public/vendor/fonts/cat-pacifico-400.woff2 +0 -0
  317. package/app/public/vendor/fonts/cat-patrick_hand-400-ext.woff2 +0 -0
  318. package/app/public/vendor/fonts/cat-patrick_hand-400.woff2 +0 -0
  319. package/app/public/vendor/fonts/cat-petit_formal_script-400-ext.woff2 +0 -0
  320. package/app/public/vendor/fonts/cat-petit_formal_script-400.woff2 +0 -0
  321. package/app/public/vendor/fonts/cat-pixelify_sans-400-ext.woff2 +0 -0
  322. package/app/public/vendor/fonts/cat-pixelify_sans-400.woff2 +0 -0
  323. package/app/public/vendor/fonts/cat-plaster-400-ext.woff2 +0 -0
  324. package/app/public/vendor/fonts/cat-plaster-400.woff2 +0 -0
  325. package/app/public/vendor/fonts/cat-playwrite_ar_guides-400-0.woff2 +0 -0
  326. package/app/public/vendor/fonts/cat-playwrite_at-400-0.woff2 +0 -0
  327. package/app/public/vendor/fonts/cat-playwrite_at_guides-400-0.woff2 +0 -0
  328. package/app/public/vendor/fonts/cat-playwrite_au_nsw-400-0.woff2 +0 -0
  329. package/app/public/vendor/fonts/cat-playwrite_au_nsw_guides-400-0.woff2 +0 -0
  330. package/app/public/vendor/fonts/cat-playwrite_au_qld-400-0.woff2 +0 -0
  331. package/app/public/vendor/fonts/cat-playwrite_au_qld_guides-400-0.woff2 +0 -0
  332. package/app/public/vendor/fonts/cat-playwrite_au_sa-400-0.woff2 +0 -0
  333. package/app/public/vendor/fonts/cat-playwrite_au_sa_guides-400-0.woff2 +0 -0
  334. package/app/public/vendor/fonts/cat-playwrite_au_tas-400-0.woff2 +0 -0
  335. package/app/public/vendor/fonts/cat-playwrite_au_tas_guides-400-0.woff2 +0 -0
  336. package/app/public/vendor/fonts/cat-playwrite_au_vic-400-0.woff2 +0 -0
  337. package/app/public/vendor/fonts/cat-playwrite_au_vic_guides-400-0.woff2 +0 -0
  338. package/app/public/vendor/fonts/cat-playwrite_be_vlg-400-0.woff2 +0 -0
  339. package/app/public/vendor/fonts/cat-playwrite_be_vlg_guides-400-0.woff2 +0 -0
  340. package/app/public/vendor/fonts/cat-playwrite_be_wal-400-0.woff2 +0 -0
  341. package/app/public/vendor/fonts/cat-playwrite_be_wal_guides-400-0.woff2 +0 -0
  342. package/app/public/vendor/fonts/cat-playwrite_br_guides-400-0.woff2 +0 -0
  343. package/app/public/vendor/fonts/cat-playwrite_ca_guides-400-0.woff2 +0 -0
  344. package/app/public/vendor/fonts/cat-playwrite_cl_guides-400-0.woff2 +0 -0
  345. package/app/public/vendor/fonts/cat-playwrite_co_guides-400-0.woff2 +0 -0
  346. package/app/public/vendor/fonts/cat-playwrite_cu_guides-400-0.woff2 +0 -0
  347. package/app/public/vendor/fonts/cat-playwrite_cz-400-0.woff2 +0 -0
  348. package/app/public/vendor/fonts/cat-playwrite_cz_guides-400-0.woff2 +0 -0
  349. package/app/public/vendor/fonts/cat-playwrite_de_grund-400-0.woff2 +0 -0
  350. package/app/public/vendor/fonts/cat-playwrite_de_grund_guides-400-0.woff2 +0 -0
  351. package/app/public/vendor/fonts/cat-playwrite_de_la-400-0.woff2 +0 -0
  352. package/app/public/vendor/fonts/cat-playwrite_de_la_guides-400-0.woff2 +0 -0
  353. package/app/public/vendor/fonts/cat-playwrite_de_sas-400-0.woff2 +0 -0
  354. package/app/public/vendor/fonts/cat-playwrite_de_sas_guides-400-0.woff2 +0 -0
  355. package/app/public/vendor/fonts/cat-playwrite_de_va-400-0.woff2 +0 -0
  356. package/app/public/vendor/fonts/cat-playwrite_de_va_guides-400-0.woff2 +0 -0
  357. package/app/public/vendor/fonts/cat-playwrite_dk_loopet-400-0.woff2 +0 -0
  358. package/app/public/vendor/fonts/cat-playwrite_dk_loopet_guides-400-0.woff2 +0 -0
  359. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet-400-0.woff2 +0 -0
  360. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet_guides-400-0.woff2 +0 -0
  361. package/app/public/vendor/fonts/cat-playwrite_es_deco_guides-400-0.woff2 +0 -0
  362. package/app/public/vendor/fonts/cat-playwrite_es_guides-400-0.woff2 +0 -0
  363. package/app/public/vendor/fonts/cat-playwrite_fr_moderne-400-0.woff2 +0 -0
  364. package/app/public/vendor/fonts/cat-playwrite_fr_moderne_guides-400-0.woff2 +0 -0
  365. package/app/public/vendor/fonts/cat-playwrite_fr_trad-400-0.woff2 +0 -0
  366. package/app/public/vendor/fonts/cat-playwrite_fr_trad_guides-400-0.woff2 +0 -0
  367. package/app/public/vendor/fonts/cat-playwrite_gb_j_guides-400-0.woff2 +0 -0
  368. package/app/public/vendor/fonts/cat-playwrite_gb_s_guides-400-0.woff2 +0 -0
  369. package/app/public/vendor/fonts/cat-playwrite_hr-400-0.woff2 +0 -0
  370. package/app/public/vendor/fonts/cat-playwrite_hr_guides-400-0.woff2 +0 -0
  371. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva-400-0.woff2 +0 -0
  372. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva_guides-400-0.woff2 +0 -0
  373. package/app/public/vendor/fonts/cat-playwrite_hu-400-0.woff2 +0 -0
  374. package/app/public/vendor/fonts/cat-playwrite_hu_guides-400-0.woff2 +0 -0
  375. package/app/public/vendor/fonts/cat-playwrite_id-400-0.woff2 +0 -0
  376. package/app/public/vendor/fonts/cat-playwrite_id_guides-400-0.woff2 +0 -0
  377. package/app/public/vendor/fonts/cat-playwrite_ie-400-0.woff2 +0 -0
  378. package/app/public/vendor/fonts/cat-playwrite_ie_guides-400-0.woff2 +0 -0
  379. package/app/public/vendor/fonts/cat-playwrite_in-400-0.woff2 +0 -0
  380. package/app/public/vendor/fonts/cat-playwrite_in_guides-400-0.woff2 +0 -0
  381. package/app/public/vendor/fonts/cat-playwrite_is-400-0.woff2 +0 -0
  382. package/app/public/vendor/fonts/cat-playwrite_is_guides-400-0.woff2 +0 -0
  383. package/app/public/vendor/fonts/cat-playwrite_it_moderna-400-0.woff2 +0 -0
  384. package/app/public/vendor/fonts/cat-playwrite_it_moderna_guides-400-0.woff2 +0 -0
  385. package/app/public/vendor/fonts/cat-playwrite_it_trad-400-0.woff2 +0 -0
  386. package/app/public/vendor/fonts/cat-playwrite_it_trad_guides-400-0.woff2 +0 -0
  387. package/app/public/vendor/fonts/cat-playwrite_mx_guides-400-0.woff2 +0 -0
  388. package/app/public/vendor/fonts/cat-playwrite_ng_modern-400-0.woff2 +0 -0
  389. package/app/public/vendor/fonts/cat-playwrite_ng_modern_guides-400-0.woff2 +0 -0
  390. package/app/public/vendor/fonts/cat-playwrite_nl-400-0.woff2 +0 -0
  391. package/app/public/vendor/fonts/cat-playwrite_nl_guides-400-0.woff2 +0 -0
  392. package/app/public/vendor/fonts/cat-playwrite_no-400-0.woff2 +0 -0
  393. package/app/public/vendor/fonts/cat-playwrite_no_guides-400-0.woff2 +0 -0
  394. package/app/public/vendor/fonts/cat-playwrite_nz-400-0.woff2 +0 -0
  395. package/app/public/vendor/fonts/cat-playwrite_nz_basic-400-0.woff2 +0 -0
  396. package/app/public/vendor/fonts/cat-playwrite_nz_basic_guides-400-0.woff2 +0 -0
  397. package/app/public/vendor/fonts/cat-playwrite_nz_guides-400-0.woff2 +0 -0
  398. package/app/public/vendor/fonts/cat-playwrite_pe_guides-400-0.woff2 +0 -0
  399. package/app/public/vendor/fonts/cat-playwrite_pl-400-0.woff2 +0 -0
  400. package/app/public/vendor/fonts/cat-playwrite_pl_guides-400-0.woff2 +0 -0
  401. package/app/public/vendor/fonts/cat-playwrite_pt_guides-400-0.woff2 +0 -0
  402. package/app/public/vendor/fonts/cat-playwrite_ro-400-0.woff2 +0 -0
  403. package/app/public/vendor/fonts/cat-playwrite_ro_guides-400-0.woff2 +0 -0
  404. package/app/public/vendor/fonts/cat-playwrite_sk-400-0.woff2 +0 -0
  405. package/app/public/vendor/fonts/cat-playwrite_sk_guides-400-0.woff2 +0 -0
  406. package/app/public/vendor/fonts/cat-playwrite_tz-400-0.woff2 +0 -0
  407. package/app/public/vendor/fonts/cat-playwrite_tz_guides-400-0.woff2 +0 -0
  408. package/app/public/vendor/fonts/cat-playwrite_us_modern_guides-400-0.woff2 +0 -0
  409. package/app/public/vendor/fonts/cat-playwrite_us_trad_guides-400-0.woff2 +0 -0
  410. package/app/public/vendor/fonts/cat-playwrite_vn-400-0.woff2 +0 -0
  411. package/app/public/vendor/fonts/cat-playwrite_vn_guides-400-0.woff2 +0 -0
  412. package/app/public/vendor/fonts/cat-playwrite_za-400-0.woff2 +0 -0
  413. package/app/public/vendor/fonts/cat-playwrite_za_guides-400-0.woff2 +0 -0
  414. package/app/public/vendor/fonts/cat-potta_one-400-ext.woff2 +0 -0
  415. package/app/public/vendor/fonts/cat-potta_one-400.woff2 +0 -0
  416. package/app/public/vendor/fonts/cat-ranchers-400-ext.woff2 +0 -0
  417. package/app/public/vendor/fonts/cat-ranchers-400.woff2 +0 -0
  418. package/app/public/vendor/fonts/cat-ranga-400-ext.woff2 +0 -0
  419. package/app/public/vendor/fonts/cat-ranga-400.woff2 +0 -0
  420. package/app/public/vendor/fonts/cat-ranga-700-ext.woff2 +0 -0
  421. package/app/public/vendor/fonts/cat-ranga-700.woff2 +0 -0
  422. package/app/public/vendor/fonts/cat-risque-400-ext.woff2 +0 -0
  423. package/app/public/vendor/fonts/cat-risque-400.woff2 +0 -0
  424. package/app/public/vendor/fonts/cat-rowdies-400-ext.woff2 +0 -0
  425. package/app/public/vendor/fonts/cat-rowdies-400.woff2 +0 -0
  426. package/app/public/vendor/fonts/cat-rowdies-700-ext.woff2 +0 -0
  427. package/app/public/vendor/fonts/cat-rowdies-700.woff2 +0 -0
  428. package/app/public/vendor/fonts/cat-ruge_boogie-400-ext.woff2 +0 -0
  429. package/app/public/vendor/fonts/cat-ruge_boogie-400.woff2 +0 -0
  430. package/app/public/vendor/fonts/cat-ruslan_display-400-ext.woff2 +0 -0
  431. package/app/public/vendor/fonts/cat-ruslan_display-400.woff2 +0 -0
  432. package/app/public/vendor/fonts/cat-sacramento-400-ext.woff2 +0 -0
  433. package/app/public/vendor/fonts/cat-sacramento-400.woff2 +0 -0
  434. package/app/public/vendor/fonts/cat-sancreek-400-ext.woff2 +0 -0
  435. package/app/public/vendor/fonts/cat-sancreek-400.woff2 +0 -0
  436. package/app/public/vendor/fonts/cat-sankofa_display-400-ext.woff2 +0 -0
  437. package/app/public/vendor/fonts/cat-sankofa_display-400.woff2 +0 -0
  438. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400-ext.woff2 +0 -0
  439. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400.woff2 +0 -0
  440. package/app/public/vendor/fonts/cat-shantell_sans-400-ext.woff2 +0 -0
  441. package/app/public/vendor/fonts/cat-shantell_sans-400.woff2 +0 -0
  442. package/app/public/vendor/fonts/cat-smokum-400-ext.woff2 +0 -0
  443. package/app/public/vendor/fonts/cat-smokum-400.woff2 +0 -0
  444. package/app/public/vendor/fonts/cat-solitreo-400-ext.woff2 +0 -0
  445. package/app/public/vendor/fonts/cat-solitreo-400.woff2 +0 -0
  446. package/app/public/vendor/fonts/cat-spicy_rice-400-ext.woff2 +0 -0
  447. package/app/public/vendor/fonts/cat-spicy_rice-400.woff2 +0 -0
  448. package/app/public/vendor/fonts/cat-sriracha-400-ext.woff2 +0 -0
  449. package/app/public/vendor/fonts/cat-sriracha-400.woff2 +0 -0
  450. package/app/public/vendor/fonts/cat-srisakdi-400-ext.woff2 +0 -0
  451. package/app/public/vendor/fonts/cat-srisakdi-400.woff2 +0 -0
  452. package/app/public/vendor/fonts/cat-srisakdi-700-ext.woff2 +0 -0
  453. package/app/public/vendor/fonts/cat-srisakdi-700.woff2 +0 -0
  454. package/app/public/vendor/fonts/cat-stix_two_math-400-0.woff2 +0 -0
  455. package/app/public/vendor/fonts/cat-tillana-400-ext.woff2 +0 -0
  456. package/app/public/vendor/fonts/cat-tillana-400.woff2 +0 -0
  457. package/app/public/vendor/fonts/cat-tillana-700-ext.woff2 +0 -0
  458. package/app/public/vendor/fonts/cat-tillana-700.woff2 +0 -0
  459. package/app/public/vendor/fonts/cat-titan_one-400-ext.woff2 +0 -0
  460. package/app/public/vendor/fonts/cat-titan_one-400.woff2 +0 -0
  461. package/app/public/vendor/fonts/cat-yatra_one-400-ext.woff2 +0 -0
  462. package/app/public/vendor/fonts/cat-yatra_one-400.woff2 +0 -0
  463. package/app/public/vendor/fonts/cat-yomogi-400-ext.woff2 +0 -0
  464. package/app/public/vendor/fonts/cat-yomogi-400.woff2 +0 -0
  465. package/app/public/vendor/fonts/dmserifdisplay-400-ext.woff2 +0 -0
  466. package/app/public/vendor/fonts/dmserifdisplay-400.woff2 +0 -0
  467. package/app/public/vendor/fonts/domine-var-ext.woff2 +0 -0
  468. package/app/public/vendor/fonts/domine-var.woff2 +0 -0
  469. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  470. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  471. package/app/public/vendor/fonts/fraunces-var-ext.woff2 +0 -0
  472. package/app/public/vendor/fonts/fraunces-var.woff2 +0 -0
  473. package/app/public/vendor/fonts/fredoka-var-ext.woff2 +0 -0
  474. package/app/public/vendor/fonts/fredoka-var.woff2 +0 -0
  475. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  476. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  477. package/app/public/vendor/fonts/jakarta-var-ext.woff2 +0 -0
  478. package/app/public/vendor/fonts/jakarta-var.woff2 +0 -0
  479. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  480. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  481. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  482. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  483. package/app/public/vendor/fonts/lora-var-ext.woff2 +0 -0
  484. package/app/public/vendor/fonts/lora-var.woff2 +0 -0
  485. package/app/public/vendor/fonts/merriweather-var-ext.woff2 +0 -0
  486. package/app/public/vendor/fonts/merriweather-var.woff2 +0 -0
  487. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  488. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  489. package/app/public/vendor/fonts/notosans-var-ext.woff2 +0 -0
  490. package/app/public/vendor/fonts/notosans-var.woff2 +0 -0
  491. package/app/public/vendor/fonts/nunito-var-ext.woff2 +0 -0
  492. package/app/public/vendor/fonts/nunito-var.woff2 +0 -0
  493. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  494. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  495. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  496. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  497. package/app/public/vendor/fonts/playfair-var-ext.woff2 +0 -0
  498. package/app/public/vendor/fonts/playfair-var.woff2 +0 -0
  499. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  500. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  501. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  502. package/app/public/vendor/fonts/quicksand-var-ext.woff2 +0 -0
  503. package/app/public/vendor/fonts/quicksand-var.woff2 +0 -0
  504. package/app/public/vendor/fonts/robotoflex-var-ext.woff2 +0 -0
  505. package/app/public/vendor/fonts/robotoflex-var.woff2 +0 -0
  506. package/app/public/vendor/fonts/sora-var-ext.woff2 +0 -0
  507. package/app/public/vendor/fonts/sora-var.woff2 +0 -0
  508. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  509. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  510. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  511. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  512. package/app/public/vendor/fonts/spacegrotesk-var-ext.woff2 +0 -0
  513. package/app/public/vendor/fonts/spacegrotesk-var.woff2 +0 -0
  514. package/app/public/vendor/fonts/spectral-400-ext.woff2 +0 -0
  515. package/app/public/vendor/fonts/spectral-400.woff2 +0 -0
  516. package/app/public/vendor/fonts/spectral-700-ext.woff2 +0 -0
  517. package/app/public/vendor/fonts/spectral-700.woff2 +0 -0
  518. package/app/public/vendor/fonts/teachers-var-ext.woff2 +0 -0
  519. package/app/public/vendor/fonts/teachers-var.woff2 +0 -0
  520. package/app/public/vendor/fonts/ubuntu-400-ext.woff2 +0 -0
  521. package/app/public/vendor/fonts/ubuntu-400.woff2 +0 -0
  522. package/app/public/vendor/fonts/ubuntu-700-ext.woff2 +0 -0
  523. package/app/public/vendor/fonts/ubuntu-700.woff2 +0 -0
  524. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  525. package/dist-pkg/platform/guide-intensity.d.ts +0 -39
  526. package/dist-pkg/platform/guide-intensity.js +0 -71
  527. package/dist-pkg/ui/latch-refusal.d.ts +0 -33
  528. package/dist-pkg/ui/latch-refusal.js +0 -60
@@ -0,0 +1,226 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/libras-avatar-player.ts — THE INTERPRETER THAT SIGNS: LAViD-UFPB's signs on a three.js avatar, behind the `Interpreter`
3
+ // port (ADR-0234, route B).
4
+ //
5
+ // 📌 THE ROOT'S OWN (phase B3): `createGame` builds this one when the host lends no interpreter (`EngineHost.interpreter`
6
+ // always wins), over the page's own address.
7
+ //
8
+ // THE RULES, as the Dev set them for the interpreter: the gloss is the delivery's (`glosses.json`, beside the avatar, where a
9
+ // word with no sign is written as the child reads it), and a text it does not cover is its words in capitals, accents stripped,
10
+ // Ç kept; the avatar appears at the bottom right when a request is played, in front of the screen, never taking the keyboard; a
11
+ // new request replaces what is being signed (the players' turns are deaf mode's); the avatar LEAVES the screen `leaveAfterMs` after it stopped signing — hidden, kept
12
+ // loaded for the next press — and a press in those seconds cancels the leaving; `hide()` takes it off at once and `dispose()`
13
+ // releases it. A delivery that did not ship it answers exactly as `NO_INTERPRETER` does («signing unavailable», with the flag
14
+ // that ships it), and a device that cannot draw it (no WebGL) or a load that never ends answers «unavailable» with its own
15
+ // reason (`ui/libras-avatar-load`).
16
+ //
17
+ // WHAT IS ITS OWN: the player is this engine's code, so «stopped» is not a message to wait for — it is the moment the last clip
18
+ // reaches its end (`ui/libras-avatar-plan`'s sequencer), its final pose held; a fingerspelled word is signed with the hand held
19
+ // up between its letters (the sequencer's chaining, the Dev's rule of 2026-09-26). And a word it can neither sign nor fingerspell is
20
+ // left out and SAID: the request still signs the rest, and what it left out comes back in `unsigned`, which deaf mode reports
21
+ // in `problems` (ADR-0169: what works in part is reported).
22
+ //
23
+ // 📌 three.js ARRIVES ONLY HERE, AND LATE: `ui/libras-avatar-stage` is imported dynamically when the avatar is first opened, so
24
+ // a page that never signs never downloads it (ADR-0234 errata; `tests/three-arrives-late.node.test.js`).
25
+ import { loadGlosser, provisionalGloss } from './libras-glosses.js';
26
+ import { createSignSequencer, leftOutText, nothingSigned, planSigns, playedLength, } from './libras-avatar-plan.js';
27
+ import { NOT_SHIPPED, RELEASED, avatarCanvas, avatarPlace, openAvatar, prepareClips, readManifest, } from './libras-avatar-load.js';
28
+ /** A school machine gets many times a fast one's load before it is given up on. */
29
+ const LOAD_TIMEOUT_MS = 60_000;
30
+ /** The Dev's five seconds (interface log, «The interpreter leaves 5 s after the player itself says it stopped»). */
31
+ const LEAVE_AFTER_MS = 5000;
32
+ /** The sharpest the avatar is drawn: above this, a school machine pays for pixels a child a metre away does not see. */
33
+ const MAX_PIXEL_RATIO = 1.5;
34
+ /** The longest step one frame may take: a tab that slept does not jump the signs ahead. */
35
+ const MAX_FRAME_S = 0.1;
36
+ /** The stage the delivery's player draws on, loaded — three.js with it — only when the avatar is first opened. */
37
+ async function stageOf(canvas, avatar) {
38
+ const { createAvatarStage } = await import('./libras-avatar-stage.js');
39
+ return createAvatarStage({ canvas, avatar });
40
+ }
41
+ /** Builds the free interpreter over the avatar the delivery shipped — or, where it shipped none, «signing unavailable». */
42
+ export function createLibrasAvatarInterpreter(ports) {
43
+ // ⚠️ `fetch` is taken OUT of the ports and called bare: the host lends `window.fetch` unbound, and called as `ports.fetch(…)`
44
+ // it would run with the ports object as `this` — an «Illegal invocation» in the browser, and no avatar.
45
+ const { doc, win, fetch: fetchFile } = ports;
46
+ const leaveAfterMs = ports.leaveAfterMs ?? LEAVE_AFTER_MS;
47
+ let place = null;
48
+ let shipped = null;
49
+ let manifest = null;
50
+ let glosser = null;
51
+ let canvas = null;
52
+ let stage = null;
53
+ let loading = null;
54
+ let cancelOpening = null;
55
+ /** Each clip is fetched and prepared once per stage. */
56
+ const clipLoads = new Map();
57
+ /** The request being signed now, answered when its last clip reached its end — with what it left out. */
58
+ let playing = null;
59
+ let playingLeftOut;
60
+ /** Moves on every `hide()` and at `dispose()`: a request that waited across one is not played. */
61
+ let generation = 0;
62
+ let disposed = false;
63
+ /** The seconds after the avatar stopped, at whose end it leaves the screen; `null` when none is running. */
64
+ let leaving = null;
65
+ let frameId = null;
66
+ let lastFrame = null;
67
+ // a spelled word's letters are chained through where each is held up, which the delivery measured (`ClipWindow.held`)
68
+ const sequencer = createSignSequencer((clip) => {
69
+ const w = manifest?.clips.get(clip);
70
+ return w ? playedLength(w) : 0;
71
+ }, undefined, (clip) => manifest?.clips.get(clip)?.held);
72
+ const signedWith = (leftOut) => (leftOut ? { signed: true, unsigned: leftOut } : { signed: true });
73
+ const answerPlaying = (r) => { const answer = playing; playing = null; answer?.(r); };
74
+ const released = () => ({ signed: false, reason: RELEASED });
75
+ /** Whether a request that started at `asked` was overtaken by a `hide()` or the root's end while it waited. */
76
+ const overtaken = (asked) => disposed || asked !== generation;
77
+ const stayOnScreen = () => {
78
+ if (leaving !== null)
79
+ win.clearTimeout(leaving);
80
+ leaving = null;
81
+ };
82
+ const stopDrawing = () => {
83
+ if (frameId !== null)
84
+ win.cancelAnimationFrame(frameId);
85
+ frameId = null;
86
+ lastFrame = null;
87
+ };
88
+ /** The avatar stopped: after `leaveAfterMs` at rest, it leaves — hidden, the stage kept loaded behind it. */
89
+ const leaveWhenAtRest = () => {
90
+ stayOnScreen();
91
+ leaving = win.setTimeout(() => {
92
+ leaving = null;
93
+ if (canvas)
94
+ canvas.hidden = true;
95
+ }, leaveAfterMs);
96
+ };
97
+ const frame = (now) => {
98
+ frameId = null;
99
+ if (!stage)
100
+ return;
101
+ const dt = lastFrame === null ? 0 : Math.min(MAX_FRAME_S, Math.max(0, (now - lastFrame) / 1000));
102
+ lastFrame = now;
103
+ const { starts, finished } = sequencer.tick(dt);
104
+ for (const s of starts)
105
+ stage.start(s.clip, s.fade, s.at);
106
+ stage.advance(dt);
107
+ if (!finished) {
108
+ frameId = win.requestAnimationFrame(frame);
109
+ return;
110
+ }
111
+ // «stopped»: the last clip reached its end and its final pose is held — nothing moves, so nothing more is drawn
112
+ lastFrame = null;
113
+ answerPlaying(signedWith(playingLeftOut));
114
+ leaveWhenAtRest();
115
+ };
116
+ /** Takes the canvas and the stage away; the next press opens them again. */
117
+ const release = () => {
118
+ stopDrawing();
119
+ cancelOpening?.();
120
+ cancelOpening = null;
121
+ stage?.dispose();
122
+ stage = null;
123
+ clipLoads.clear();
124
+ canvas?.remove();
125
+ canvas = null;
126
+ loading = null;
127
+ };
128
+ /** Opens the canvas and the stage once, and answers when the avatar is ready to sign — or why it cannot. */
129
+ const load = (at, m) => {
130
+ if (loading)
131
+ return loading;
132
+ const made = avatarCanvas(doc, ports.title());
133
+ canvas = made;
134
+ const opening = openAvatar(made, {
135
+ fetchFile, avatarUrl: new URL(m.avatar, at.folder).href, loadStage: ports.loadStage ?? stageOf,
136
+ timeoutMs: ports.loadTimeoutMs ?? LOAD_TIMEOUT_MS, win,
137
+ }, () => canvas === made);
138
+ cancelOpening = opening.cancel;
139
+ loading = opening.done.then((r) => {
140
+ if ('stage' in r) {
141
+ stage = r.stage;
142
+ return { signed: true };
143
+ }
144
+ if (canvas === made)
145
+ release(); // the next press tries again, from a fresh canvas
146
+ return { signed: false, reason: r.reason };
147
+ });
148
+ return loading;
149
+ };
150
+ /** The delivery's avatar, ready to sign: its place and manifest — or the answer the request gets instead. */
151
+ const reach = async (asked) => {
152
+ const at = (place ??= avatarPlace(ports.base, ports.avatarFolder));
153
+ const m = at ? await (shipped ??= readManifest(fetchFile, at)) : null;
154
+ if (!at || !m)
155
+ return { signed: false, reason: NOT_SHIPPED };
156
+ manifest = m;
157
+ if (overtaken(asked))
158
+ return released();
159
+ const loaded = await load(at, m);
160
+ return loaded.signed ? { at, m } : loaded;
161
+ };
162
+ /** The clips a text is signed with, all prepared, and what was left out — or the answer the request gets instead. */
163
+ const planFor = async (text, at, m, asked) => {
164
+ const glossOf = await (glosser ??= loadGlosser(fetchFile, at.glosses));
165
+ const plan = planSigns(glossOf(text) || provisionalGloss(text), (name) => m.clips.has(name));
166
+ const unread = stage ? await prepareClips(stage, [...new Set(plan.steps.map((s) => s.clip))], m, at, fetchFile, clipLoads) : [];
167
+ if (overtaken(asked) || !stage || !canvas)
168
+ return released();
169
+ const steps = plan.steps.filter((s) => !unread.includes(s.clip));
170
+ const leftOut = leftOutText(plan.unsigned, unread);
171
+ return steps.length ? { steps, leftOut } : { signed: false, reason: nothingSigned(leftOut) };
172
+ };
173
+ /** Puts the avatar in front of the screen and signs `steps`; answered when the last one reached its end. */
174
+ const perform = (on, shown, steps, leftOut) => {
175
+ // 📌 A new request replaces what is being signed. The turns among players are deaf mode's (`ui/vlibras`, ADR-0234 errata
176
+ // 2026-09-26): it asks again only for the same player, or for a request that names none. The one replaced is not a
177
+ // failure: the child asked again.
178
+ answerPlaying(signedWith(playingLeftOut));
179
+ shown.hidden = false;
180
+ const box = shown.getBoundingClientRect();
181
+ on.resize(box.width, box.height, Math.min(MAX_PIXEL_RATIO, win.devicePixelRatio || 1));
182
+ for (const s of sequencer.play(steps))
183
+ on.start(s.clip, s.fade, s.at);
184
+ lastFrame = null;
185
+ if (frameId === null)
186
+ frameId = win.requestAnimationFrame(frame);
187
+ return new Promise((resolve) => {
188
+ playing = resolve;
189
+ playingLeftOut = leftOut;
190
+ });
191
+ };
192
+ return {
193
+ sign: async (text) => {
194
+ // nothing to sign is not a failure, and nothing goes to `problems`
195
+ if (!provisionalGloss(text))
196
+ return { signed: true };
197
+ // a press in the seconds at rest keeps the avatar where it is: it is about to sign again
198
+ stayOnScreen();
199
+ const asked = generation;
200
+ const reached = await reach(asked);
201
+ if ('signed' in reached)
202
+ return reached;
203
+ const plan = await planFor(text, reached.at, reached.m, asked);
204
+ if ('signed' in plan || !stage || !canvas)
205
+ return 'signed' in plan ? plan : released();
206
+ return perform(stage, canvas, plan.steps, plan.leftOut);
207
+ },
208
+ hide: () => {
209
+ generation += 1;
210
+ stayOnScreen();
211
+ sequencer.stop();
212
+ stopDrawing();
213
+ if (canvas)
214
+ canvas.hidden = true;
215
+ answerPlaying(released());
216
+ },
217
+ dispose: () => {
218
+ disposed = true;
219
+ generation += 1;
220
+ stayOnScreen(); // no timer of this root outlives it
221
+ sequencer.stop();
222
+ answerPlaying(released());
223
+ release();
224
+ },
225
+ };
226
+ }
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { type ClipJson } from './libras-avatar-clip.js';
3
+ /** What the player drives. */
4
+ export interface AvatarStage {
5
+ /** The avatar's three.js root, for a test or a measurement that reads the pose. Nothing in the engine reads it. */
6
+ readonly scene: object;
7
+ /** Makes a clip playable, from `from` seconds of it on. */
8
+ readonly prepare: (name: string, clip: ClipJson, from: number) => void;
9
+ readonly has: (name: string) => boolean;
10
+ /** Puts a prepared clip on the stage, fading in over `fade` seconds from what is there, `at` seconds into it. */
11
+ readonly start: (name: string, fade: number, at: number) => void;
12
+ /** Moves the clips `dt` seconds and draws. */
13
+ readonly advance: (dt: number) => void;
14
+ /** The canvas's size in CSS pixels and the pixel ratio it is drawn at. */
15
+ readonly resize: (width: number, height: number, pixelRatio: number) => void;
16
+ /** What the drawing costs, for a measurement: the renderer's own counters. */
17
+ readonly stats: () => {
18
+ readonly textures: number;
19
+ readonly geometries: number;
20
+ readonly calls: number;
21
+ readonly triangles: number;
22
+ };
23
+ /** Releases the GPU and everything the avatar holds. */
24
+ readonly dispose: () => void;
25
+ }
26
+ export interface AvatarStageOptions {
27
+ /** The canvas the avatar is drawn on: the player's, already in its document. */
28
+ readonly canvas: HTMLCanvasElement;
29
+ /** The delivery's `avatar.glb`, as bytes. */
30
+ readonly avatar: ArrayBuffer;
31
+ readonly textureSize?: number;
32
+ }
33
+ /**
34
+ * Builds the stage over `canvas`. THROWS where it cannot draw — no WebGL, an avatar three.js cannot read — and the player turns
35
+ * that into «signing unavailable», with the reason.
36
+ */
37
+ export declare function createAvatarStage({ canvas, avatar, textureSize }: AvatarStageOptions): Promise<AvatarStage>;
@@ -0,0 +1,208 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/libras-avatar-stage — THE FREE PLAYER'S STAGE: three.js, the avatar and its clips (ADR-0234, route B, phase B2).
3
+ //
4
+ // 📌 THE ONLY MODULE THAT NAMES three.js, and it is reached only by the dynamic `import()` in `ui/libras-avatar-player`, at the
5
+ // first sign in deaf mode (ADR-0234 errata): a child who never turns deaf mode on never downloads three.js. three.js 0.186.1,
6
+ // MIT, a runtime `dependency` pinned to that exact version; the types are `three-subset.d.ts`, beside this file.
7
+ //
8
+ // WHAT IT DOES: parses the delivery's `avatar.glb` (GLTFLoader, from the bytes the player fetched — no loader fetches anything),
9
+ // frames the signing space, and plays clips on one `AnimationMixer`, each once, its last frame held. Two things the export
10
+ // hands over are answered here and not hidden:
11
+ // · the face: a morph track names the head's node, which three.js loads as a group of two meshes — `bindFaceTracks` writes
12
+ // it once for each mesh that carries the targets;
13
+ // · the bones a clip does not move: the export dropped the tracks at the avatar's rest, so a clip that follows one that moved
14
+ // them must bring them back. The mixer does it: a property no running clip drives is blended back to the value it had when
15
+ // it was first bound — the avatar's rest, since nothing else ever poses it — and an action whose fade-out ended is stopped,
16
+ // which restores it outright (`tests/libras-avatar.browser.test.js` holds it).
17
+ //
18
+ // 🔴 TEXTURES ARE DECODED HERE, NOT BY THE LOADER'S OWN IMAGE LOADER: three.js reads an embedded image by `fetch`ing a `blob:`
19
+ // URL, which the delivery's `connect-src 'self'` refuses. `createImageBitmap` over the bytes fetches nothing, and it is also
20
+ // where the avatar's 2048² textures are brought down to `TEXTURE_SIZE` — the interpreter is a few hundred pixels tall.
21
+ import { AnimationClip, AnimationMixer, Box3, DirectionalLight, HemisphereLight, LoopOnce, PerspectiveCamera, Scene, Vector3, WebGLRenderer, } from 'three';
22
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
23
+ import { bindFaceTracks, clipFrom } from './libras-avatar-clip.js';
24
+ /**
25
+ * The longest side a texture keeps, in pixels. 📏 The export's head and body textures are 2048² each: 16 MiB of pixels apiece
26
+ * once decoded, 21 with mipmaps; at 1024² a quarter of that. The avatar is drawn a few hundred pixels tall.
27
+ */
28
+ const TEXTURE_SIZE = 1024;
29
+ const isMesh = (o) => o.isMesh === true;
30
+ /** The loader's texture step, replaced: the embedded image decoded from its bytes (no `blob:` fetch), then brought down to `size`. */
31
+ function decodedTextures(size) {
32
+ return (parser) => ({
33
+ name: 'incl_decoded_textures',
34
+ loadTexture: (textureIndex) => {
35
+ const texture = parser.json.textures?.[textureIndex];
36
+ const source = texture?.source;
37
+ const image = source === undefined ? undefined : parser.json.images?.[source];
38
+ if (source === undefined || image?.bufferView === undefined)
39
+ return null; // not embedded: the loader's own path
40
+ return parser.getDependency('bufferView', image.bufferView).then(async (bytes) => {
41
+ const blob = new Blob([bytes], { type: image.mimeType ?? 'image/png' });
42
+ let bitmap = await createImageBitmap(blob, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' });
43
+ const scale = Math.min(1, size / Math.max(bitmap.width, bitmap.height));
44
+ if (scale < 1) {
45
+ const full = bitmap;
46
+ bitmap = await createImageBitmap(full, {
47
+ premultiplyAlpha: 'none', colorSpaceConversion: 'none', resizeQuality: 'high',
48
+ resizeWidth: Math.max(1, Math.round(full.width * scale)), resizeHeight: Math.max(1, Math.round(full.height * scale)),
49
+ });
50
+ full.close();
51
+ }
52
+ const decoded = bitmap;
53
+ return parser.loadTextureImage(textureIndex, source, {
54
+ isImageBitmapLoader: true,
55
+ load: (_url, onLoad) => { onLoad(decoded); },
56
+ });
57
+ });
58
+ },
59
+ });
60
+ }
61
+ /**
62
+ * The meshes a morph track on each node must reach: for a node that is not itself a mesh with morph targets but holds meshes
63
+ * that have them (the head, loaded as a group of two), their uuids — three.js binds a track by uuid as well as by name.
64
+ */
65
+ function faceMeshes(root) {
66
+ const out = new Map();
67
+ root.traverse((node) => {
68
+ if (isMesh(node) && node.morphTargetDictionary)
69
+ return;
70
+ const meshes = node.children.filter((c) => isMesh(c) && !!c.morphTargetDictionary).map((c) => c.uuid);
71
+ if (meshes.length && node.name)
72
+ out.set(node.name, meshes);
73
+ });
74
+ return out;
75
+ }
76
+ /**
77
+ * The signing space, framed on the head (the mesh with morph targets): from just above it to below the waist, where Libras signs
78
+ * are made. 📏 Tuned on the export's avatar (screenshot of 2026-09-25): its head mesh runs from the hair's base to the neck, and
79
+ * 1.45 of its heights below it is a little under the belt — the face about a quarter of the frame's height.
80
+ */
81
+ function frame(root, camera) {
82
+ root.updateMatrixWorld(true);
83
+ const face = new Box3();
84
+ root.traverse((node) => { if (isMesh(node) && node.morphTargetDictionary)
85
+ face.expandByObject(node); });
86
+ const box = face.isEmpty() ? new Box3().setFromObject(root) : face;
87
+ const center = box.getCenter(new Vector3());
88
+ const head = box.getSize(new Vector3()).y || 1;
89
+ const top = box.max.y + 0.15 * head;
90
+ const bottom = box.min.y - 1.45 * head;
91
+ const middle = (top + bottom) / 2;
92
+ const tall = top - bottom;
93
+ const fov = 30;
94
+ const distance = (tall / 2) / Math.tan((fov / 2) * Math.PI / 180);
95
+ camera.position.set(center.x, middle, box.max.z + distance);
96
+ camera.lookAt(center.x, middle, center.z);
97
+ camera.updateProjectionMatrix();
98
+ }
99
+ /**
100
+ * Builds the stage over `canvas`. THROWS where it cannot draw — no WebGL, an avatar three.js cannot read — and the player turns
101
+ * that into «signing unavailable», with the reason.
102
+ */
103
+ export async function createAvatarStage({ canvas, avatar, textureSize = TEXTURE_SIZE }) {
104
+ const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: 'low-power' });
105
+ renderer.setClearColor(0x000000, 0);
106
+ let root;
107
+ try {
108
+ const loader = new GLTFLoader().register(decodedTextures(textureSize));
109
+ root = (await loader.parseAsync(avatar, '')).scene;
110
+ }
111
+ catch (failure) {
112
+ renderer.dispose();
113
+ renderer.forceContextLoss();
114
+ throw failure;
115
+ }
116
+ const scene = new Scene();
117
+ const camera = new PerspectiveCamera(30, 1, 0.05, 200);
118
+ const sky = new HemisphereLight(0xffffff, 0x8a8a9a, 2.2);
119
+ const key = new DirectionalLight(0xffffff, 1.6);
120
+ key.position.set(2, 6, 10);
121
+ scene.add(root, sky, key);
122
+ frame(root, camera);
123
+ const mixer = new AnimationMixer(root);
124
+ const meshesOf = faceMeshes(root);
125
+ /** Each clip twice: the same sign signed twice in a row fades into its own copy, not into itself. */
126
+ const clips = new Map();
127
+ let current = null;
128
+ /** Actions fading out or held: stopped once their weight reaches nothing, which returns what they drove to rest. */
129
+ const leaving = new Set();
130
+ const draw = () => { renderer.render(scene, camera); };
131
+ return {
132
+ scene: root,
133
+ prepare: (name, json, from) => {
134
+ const bound = bindFaceTracks(clipFrom(json, from), (node) => meshesOf.get(node) ?? null);
135
+ // 🔴 `AnimationClip.parse` copies the JSON's `uuid`, and the export writes none: every parsed clip has the SAME uuid
136
+ // (`undefined`), and the mixer keys its actions by uuid — so every clip played as the first one ever asked for. A clone
137
+ // is built by the constructor, which gives it a uuid of its own.
138
+ const clip = AnimationClip.parse(bound).clone();
139
+ clip.name = name;
140
+ clips.set(name, [clip, clip.clone()]);
141
+ },
142
+ has: (name) => clips.has(name),
143
+ start: (name, fade, at) => {
144
+ const pair = clips.get(name);
145
+ if (!pair)
146
+ return;
147
+ const clip = current?.getClip() === pair[0] ? pair[1] : pair[0];
148
+ const action = mixer.clipAction(clip);
149
+ leaving.delete(action);
150
+ action.reset();
151
+ action.setLoop(LoopOnce, 1);
152
+ action.clampWhenFinished = true;
153
+ action.time = at;
154
+ action.play();
155
+ if (current) {
156
+ if (fade > 0) {
157
+ action.crossFadeFrom(current, fade, false);
158
+ leaving.add(current);
159
+ }
160
+ else
161
+ current.stop();
162
+ }
163
+ current = action;
164
+ },
165
+ advance: (dt) => {
166
+ mixer.update(dt);
167
+ for (const action of leaving) {
168
+ if (!action.enabled) {
169
+ action.stop();
170
+ leaving.delete(action);
171
+ }
172
+ }
173
+ draw();
174
+ },
175
+ resize: (width, height, pixelRatio) => {
176
+ renderer.setPixelRatio(pixelRatio);
177
+ renderer.setSize(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)), false);
178
+ camera.aspect = width > 0 && height > 0 ? width / height : 1;
179
+ camera.updateProjectionMatrix();
180
+ draw();
181
+ },
182
+ stats: () => ({
183
+ textures: renderer.info.memory.textures, geometries: renderer.info.memory.geometries,
184
+ calls: renderer.info.render.calls, triangles: renderer.info.render.triangles,
185
+ }),
186
+ dispose: () => {
187
+ mixer.stopAllAction();
188
+ mixer.uncacheRoot(root);
189
+ root.traverse((node) => {
190
+ if (!isMesh(node))
191
+ return;
192
+ node.geometry.dispose();
193
+ for (const material of Array.isArray(node.material) ? node.material : [node.material]) {
194
+ for (const value of Object.values(material)) {
195
+ const texture = value;
196
+ if (texture?.isTexture) {
197
+ texture.dispose?.();
198
+ texture.image?.close?.();
199
+ }
200
+ }
201
+ material.dispose();
202
+ }
203
+ });
204
+ renderer.dispose();
205
+ renderer.forceContextLoss();
206
+ },
207
+ };
208
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** The file the delivery writes beside the avatar (`LIBRAS_AVATAR_FOLDER`), read by the interpreter. */
3
+ export declare const LIBRAS_GLOSSES_FILE = "glosses.json";
4
+ /** The file's shape. `glosses` holds `[text, gloss]` pairs; a text with `{name}` holes is a template, its gloss has them too. */
5
+ export interface GlossFile {
6
+ readonly format: 1;
7
+ /** What made the glosses, e.g. `vlibras-translator 1.3.3 (rules) · pt_core_news_md 3.8.0`. For whoever reads the file. */
8
+ readonly made: string;
9
+ readonly glosses: readonly (readonly [text: string, gloss: string])[];
10
+ }
11
+ /** The text a player is handed for a text on screen. */
12
+ export type Glosser = (text: string) => string;
13
+ /**
14
+ * The text as the player is handed it where no gloss is known: its words in capitals, accents stripped but Ç kept, anything
15
+ * that is not a letter or a digit a separator. Not a gloss — see the header. The marks are stripped from each run of text
16
+ * with no Ç in it: Ç is the only letter with a mark that the manual alphabet signs as itself.
17
+ */
18
+ export declare function provisionalGloss(text: string): string;
19
+ /**
20
+ * The glosser over a delivered file: known texts and templates first, today's rule for what neither covers. A file that is not
21
+ * the delivery's (`null`, a fallback page, another format) glosses nothing, and every text gets today's rule.
22
+ */
23
+ export declare function glosserOf(data: unknown): Glosser;
24
+ /**
25
+ * The glosser of the delivery the page came from: `glosses.json` read through `fetchFile`, or — no fetch, no file, not the
26
+ * delivery's — today's rule for every text. Never rejects: a missing file costs the glosses, not the signing.
27
+ */
28
+ export declare function loadGlosser(fetchFile: ((url: string) => Promise<Response>) | undefined, url: string): Promise<Glosser>;
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // ui/libras-glosses — WHAT THE INTERPRETER HANDS THE PLAYER: the Libras gloss of the text the sonar found (ADR-0234, plan
3
+ // item 5b).
4
+ //
5
+ // The player signs a GLOSS — Libras word order, in sign names — and the sonar finds Portuguese. Translating needs spaCy's full
6
+ // Portuguese parser, which does not run in a browser, and it does not have to: the text a child can be shown is AUTHORED, so the
7
+ // delivery glosses it once, at build time, with LAViD's rule-based `vlibras-translator` (`scripts/libras-glosses.mjs`, run by
8
+ // `inclusionist-heavy --libras`), and writes `glosses.json` beside the avatar. At run time nothing is translated: the text is
9
+ // LOOKED UP here.
10
+ //
11
+ // WHAT IS IN THE FILE: the engine's own Portuguese dictionary, and the game's when its delivery names it (`--libras-texts`).
12
+ // A string with `{param}` holes is kept as a TEMPLATE, glossed with its holes in place (`JOGADOR {n} ENTRAR [EXCLAMAÇÃO]`),
13
+ // because the value arrives only at run time: here the value is looked up in turn, and a value with no gloss of its own — a
14
+ // number, a name — is handed over by today's rule below, which the player fingerspells letter by letter.
15
+ //
16
+ // HOW A SCREEN IS LOOKED UP: the sonar reads the screen as sentences (`ui/screen-text`, one per line, joined), so the text is
17
+ // cut at sentence ends and, from the first sentence on, the LONGEST run of sentences that is a known text or matches a template
18
+ // is taken — a dictionary string may itself hold two sentences, or a colon the reader cut at. A sentence nothing matches falls
19
+ // back to today's rule, alone; the rest of the screen keeps its glosses.
20
+ //
21
+ // 📌 TODAY'S RULE (`provisionalGloss`), which stays the fallback: the words in capitals with the accents stripped, anything that
22
+ // is not a letter or a digit a separator. Not a gloss — the player fingerspells it — and the accents go because the player has
23
+ // no clip for an accented letter (the avatar's alphabet is A–Z and Ç, `scripts/libras-avatar.json`). Ç IS KEPT: it is a letter of
24
+ // the Libras manual alphabet, with its own handshape and movement, and the avatar carries its clip. The build spells with the same
25
+ // rule (`scripts/libras-glosses.mjs` `spelledWord`, ADR-0234 erratum «A WORD WITH NO SIGN IS SPELLED AS IT IS WRITTEN»).
26
+ //
27
+ // It reaches no global: the file comes through the fetch the interpreter was given (ADR-0232).
28
+ /** The file the delivery writes beside the avatar (`LIBRAS_AVATAR_FOLDER`), read by the interpreter. */
29
+ export const LIBRAS_GLOSSES_FILE = 'glosses.json';
30
+ /**
31
+ * The text as the player is handed it where no gloss is known: its words in capitals, accents stripped but Ç kept, anything
32
+ * that is not a letter or a digit a separator. Not a gloss — see the header. The marks are stripped from each run of text
33
+ * with no Ç in it: Ç is the only letter with a mark that the manual alphabet signs as itself.
34
+ */
35
+ export function provisionalGloss(text) {
36
+ return text.normalize('NFC').replace(/[^Çç]+/gu, (run) => run.normalize('NFD').replace(/\p{M}+/gu, '')).toUpperCase()
37
+ .split(/[^\p{L}\p{N}]+/u).filter(Boolean).join(' ');
38
+ }
39
+ /** Where the sonar's text is cut: after a sentence's end, before the next. */
40
+ const SENTENCE_END = /(?<=[.!?…:;])\s+/u;
41
+ /** A hole of a template (`split` and `replace` read the capture; `HAS_HOLE` only asks). */
42
+ const HOLE = /\{([^{}\s]+)\}/gu;
43
+ const HAS_HOLE = /\{[^{}\s]+\}/u;
44
+ const NUMBERED_HOLE = /\{\d+\}/gu;
45
+ /**
46
+ * What a hole matches: any run of characters that does not cross a sentence's end — a template that starts or ends with a hole
47
+ * would otherwise swallow the sentences around it when a longer run is tried.
48
+ */
49
+ const VALUE = String.raw `((?:(?![.!?…:;]\s).)+?)`;
50
+ /** What the edges of a text may carry that does not change it: punctuation, an icon, a space. */
51
+ const EDGES = /^[^\p{L}\p{N}{}]+|[^\p{L}\p{N}{}]+$/gu;
52
+ /** The same text, however the screen wrote it: one space, no punctuation or icon at the edges, lower case. */
53
+ function keyOf(text) {
54
+ return text.normalize('NFC').replace(/\s+/gu, ' ').replace(EDGES, '').toLocaleLowerCase('pt-BR');
55
+ }
56
+ const sentencesOf = (text) => text.split(SENTENCE_END).filter((s) => s.trim() !== '');
57
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
+ /** A template's text as a pattern over keys: its fixed parts as keys are, each hole a `VALUE`. */
59
+ function templateOf(text, gloss) {
60
+ const names = [];
61
+ const parts = text.split(HOLE); // even indices are fixed text, odd ones hole names
62
+ for (let i = 1; i < parts.length; i += 2)
63
+ names.push(parts[i]);
64
+ // each hole becomes `{0}`, `{1}`…: braces survive `keyOf`, and a name's capitals would not
65
+ const marked = parts.map((p, i) => (i % 2 ? `{${(i - 1) / 2}}` : p)).join('');
66
+ const key = keyOf(marked);
67
+ const fixed = key.replace(NUMBERED_HOLE, '');
68
+ if (!/[\p{L}\p{N}]/u.test(fixed))
69
+ return null; // all holes: it would match anything
70
+ const source = key.split(NUMBERED_HOLE).map(escapeRegExp).join(VALUE);
71
+ return { pattern: new RegExp(`^${source}$`, 'u'), names, gloss, fixed: fixed.length };
72
+ }
73
+ /** Is this a file the delivery wrote? Anything else is read as no file at all. */
74
+ function isGlossFile(data) {
75
+ const file = data;
76
+ return !!file && file.format === 1 && Array.isArray(file.glosses)
77
+ && file.glosses.every((g) => Array.isArray(g) && typeof g[0] === 'string' && typeof g[1] === 'string');
78
+ }
79
+ /**
80
+ * The glosser over a delivered file: known texts and templates first, today's rule for what neither covers. A file that is not
81
+ * the delivery's (`null`, a fallback page, another format) glosses nothing, and every text gets today's rule.
82
+ */
83
+ export function glosserOf(data) {
84
+ const exact = new Map();
85
+ const templates = [];
86
+ let longest = 1;
87
+ if (isGlossFile(data)) {
88
+ for (const [text, gloss] of data.glosses) {
89
+ if (!gloss.trim())
90
+ continue;
91
+ if (HAS_HOLE.test(text)) {
92
+ const template = templateOf(text, gloss);
93
+ if (template)
94
+ templates.push(template);
95
+ }
96
+ else {
97
+ const key = keyOf(text);
98
+ if (key && !exact.has(key))
99
+ exact.set(key, gloss);
100
+ }
101
+ longest = Math.max(longest, sentencesOf(text).length);
102
+ }
103
+ templates.sort((a, b) => b.fixed - a.fixed);
104
+ }
105
+ /** A value put into a template's hole: its own gloss when it is a known text, else today's rule. */
106
+ const valueGloss = (value) => exact.get(keyOf(value)) ?? provisionalGloss(value);
107
+ const lookUp = (piece) => {
108
+ const key = keyOf(piece);
109
+ if (!key)
110
+ return null;
111
+ const known = exact.get(key);
112
+ if (known !== undefined)
113
+ return known;
114
+ for (const t of templates) {
115
+ const m = t.pattern.exec(key);
116
+ if (!m)
117
+ continue;
118
+ const values = new Map(t.names.map((name, i) => [name, valueGloss(m[i + 1])]));
119
+ return t.gloss.replace(HOLE, (_, name) => values.get(name) ?? '').replace(/\s+/gu, ' ').trim();
120
+ }
121
+ return null;
122
+ };
123
+ return (text) => {
124
+ const sentences = sentencesOf(text);
125
+ const out = [];
126
+ for (let i = 0; i < sentences.length;) {
127
+ let taken = 0;
128
+ for (let span = Math.min(longest, sentences.length - i); span >= 1 && !taken; span--) {
129
+ const gloss = lookUp(sentences.slice(i, i + span).join(' '));
130
+ if (gloss !== null) {
131
+ if (gloss)
132
+ out.push(gloss);
133
+ taken = span;
134
+ }
135
+ }
136
+ if (!taken) {
137
+ const spelled = provisionalGloss(sentences[i]);
138
+ if (spelled)
139
+ out.push(spelled);
140
+ taken = 1;
141
+ }
142
+ i += taken;
143
+ }
144
+ return out.join(' ');
145
+ };
146
+ }
147
+ /**
148
+ * The glosser of the delivery the page came from: `glosses.json` read through `fetchFile`, or — no fetch, no file, not the
149
+ * delivery's — today's rule for every text. Never rejects: a missing file costs the glosses, not the signing.
150
+ */
151
+ export async function loadGlosser(fetchFile, url) {
152
+ if (!fetchFile)
153
+ return glosserOf(null);
154
+ try {
155
+ const resp = await fetchFile(url);
156
+ return glosserOf(resp.ok ? await resp.json() : null);
157
+ }
158
+ catch {
159
+ return glosserOf(null);
160
+ }
161
+ }