cuju-render 0.1.0__py3-none-any.whl

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 (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,488 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The diagnostic report of the Help page. One action gathers what the browser can do, WebGL,
5
+ // WebGPU, the decode worker, the sound and the storage, with the page's version and settings,
6
+ // and asks the server for its own side, the versions, the home folder, the library and the recent
7
+ // errors, as `cuju render doctor` prints them. The report is text to copy into an issue or a file
8
+ // to save. It holds no personal path beyond the home folder, which the server writes from `~`, and
9
+ // no secret. Making it never plays a sound and never decodes a recording. Scripts reach it as
10
+ // window.cuju.doctor. reportText at the top of the file touches nothing of the page, so it holds
11
+ // the same in a test.
12
+
13
+ import { lastDecoder } from '../recording/decode.js';
14
+ import { formatList, formatNumber, serverText, t, tOr, tn } from '../core/i18n.js';
15
+ import { icon, symbolOnly } from '../core/icons.js';
16
+
17
+ function h(tag, attributes = {}, ...children) {
18
+ const node = document.createElement(tag);
19
+ for (const [key, value] of Object.entries(attributes)) {
20
+ if (key.startsWith('on') && typeof value === 'function') {
21
+ node.addEventListener(key.slice(2), value);
22
+ } else if (value !== null && value !== undefined && value !== false) {
23
+ node.setAttribute(key, value === true ? '' : value);
24
+ }
25
+ }
26
+ node.append(...children.flat().filter((child) => child !== null && child !== undefined));
27
+ return node;
28
+ }
29
+
30
+ const yes = (value) => (value ? t('doctor.yes') : t('doctor.no'));
31
+
32
+ // A number with one decimal, in the language's way, without grouping, as the server writes it.
33
+ const oneDecimal = (value) => formatNumber(value, { minimumFractionDigits: 1, maximumFractionDigits: 1, useGrouping: false });
34
+
35
+ function size(bytes) {
36
+ if (typeof bytes !== 'number' || !Number.isFinite(bytes)) {
37
+ return t('doctor.unknown');
38
+ }
39
+ const units = ['bytes', 'KB', 'MB', 'GB'];
40
+ let value = bytes;
41
+ let unit = 0;
42
+ while (value >= 1024 && unit < units.length - 1) {
43
+ value /= 1024;
44
+ unit++;
45
+ }
46
+ return unit === 0 ? t('doctor.size.bytes', { count: value }) : `${oneDecimal(value)} ${units[unit]}`;
47
+ }
48
+
49
+ // The checks of the server by the words of their details. Each name is the message
50
+ // doctor.check.<key>.name, and a detail that one of the patterns matches is the message
51
+ // doctor.check.<key>.<variant>, with the named parts of the pattern as its values. Any other
52
+ // detail shows as the server wrote it.
53
+ const CHECKS = {
54
+ 'home folder': ['home_folder', [
55
+ ['file', /^the home folder is a file$/],
56
+ ['under', /^the home folder lies under a file$/],
57
+ ['missing', /^the home folder is missing, the server makes it as it starts$/],
58
+ ['readonly', /^the home folder is not writable$/],
59
+ ['exists', /^(?<path>.+) exists$/],
60
+ ]],
61
+ writing: ['writing', [
62
+ ['ok', /^the server can write its folder$/],
63
+ ['fail', /^the server cannot write its folder, choose another with --home$/],
64
+ ]],
65
+ 'disk space': ['disk_space', [
66
+ ['unknown', /^unknown$/],
67
+ ['low', /^(?<size>\d+\.\d) GB free, recordings may fail to save$/],
68
+ ['free', /^(?<size>\d+\.\d) GB free$/],
69
+ ]],
70
+ samples: ['samples', [
71
+ ['count', /^(?<count>\d+) sample recordings ship with the package$/],
72
+ ]],
73
+ 'page files': ['page_files', [
74
+ ['ok', /^the files of the page are in place$/],
75
+ ['missing', /^the page is missing, install cuju again$/],
76
+ ]],
77
+ 'server file': ['server_file', [
78
+ ['stale', /^a stale server\.json names a server that does not answer, the next start replaces it$/],
79
+ ['running', /^the server of the folder answers$/],
80
+ ['none', /^no server runs$/],
81
+ ]],
82
+ 'video export': ['video_export', [
83
+ ['found', /^a browser is found, (?<browser>.+)$/],
84
+ ]],
85
+ };
86
+
87
+ // The name of a check of the server in the page's language.
88
+ // The status line of a report. It names the checks that fail and those that warn, and says that
89
+ // every check passes only when none does either.
90
+ export function reportStatus(report) {
91
+ const server = report && report.server;
92
+ if (!server || server.error) {
93
+ return t('doctor.status.noServer');
94
+ }
95
+ const checks = Array.isArray(server.checks) ? server.checks : [];
96
+ const failed = checks.filter((check) => !check.ok);
97
+ const warned = checks.filter((check) => check.ok && check.warning);
98
+ if (!failed.length && !warned.length) {
99
+ return t('doctor.status.passing');
100
+ }
101
+ const parts = [];
102
+ if (failed.length) {
103
+ parts.push(t('doctor.status.failing', { count: failed.length, names: formatList(failed.map(checkName)) }));
104
+ }
105
+ if (warned.length) {
106
+ parts.push(t('doctor.status.warning', { count: warned.length, names: formatList(warned.map(checkName)) }));
107
+ }
108
+ return t('doctor.status.ready', { parts: parts.join(' ') });
109
+ }
110
+
111
+ export function checkName(check) {
112
+ if (check.name_id) {
113
+ return serverText(check, 'name');
114
+ }
115
+ const known = CHECKS[check.name];
116
+ return known ? tOr(`doctor.check.${known[0]}.name`, check.name) : String(check.name ?? '');
117
+ }
118
+
119
+ // The detail of a check of the server in the page's language.
120
+ export function checkDetail(check) {
121
+ if (check.detail_id) {
122
+ return serverText(check, 'detail');
123
+ }
124
+ const detail = String(check.detail ?? '');
125
+ const known = CHECKS[check.name];
126
+ if (!known) {
127
+ return detail;
128
+ }
129
+ for (const [variant, pattern] of known[1]) {
130
+ const match = detail.match(pattern);
131
+ if (match) {
132
+ const values = { ...(match.groups || {}) };
133
+ if (values.size !== undefined) {
134
+ values.size = oneDecimal(Number(values.size));
135
+ }
136
+ if (values.count !== undefined) {
137
+ values.count = Number(values.count);
138
+ }
139
+ return tOr(`doctor.check.${known[0]}.${variant}`, detail, values);
140
+ }
141
+ }
142
+ return detail;
143
+ }
144
+
145
+ // The report as lines of text, the page's side first and then the server's.
146
+ export function reportText(report) {
147
+ const page = report.page || {};
148
+ const lines = [t('doctor.report.head', { created: report.created || '' }), ''];
149
+ // Each section's title is the message doctor.section.<key>, and each row's name the message
150
+ // doctor.row.<key>, unless the row names a package, a folder or a count of the server.
151
+ const row = (key, value) => [t(`doctor.row.${key}`), value];
152
+ const section = (key, rows) => {
153
+ lines.push(t(`doctor.section.${key}`));
154
+ for (const [name, value] of rows) {
155
+ lines.push(` ${name}: ${value === null || value === undefined || value === '' ? t('doctor.unknown') : value}`);
156
+ }
157
+ lines.push('');
158
+ };
159
+ section('page', [
160
+ row('version', page.version), row('address', page.origin), row('page', page.page), row('drawing', page.drawing),
161
+ row('reducedMotion', yes(page.reducedMotion)), row('theme', page.theme), row('installed', page.standalone ? t('doctor.installed.app') : t('doctor.no')),
162
+ row('serviceWorker', page.serviceWorker), row('videoExport', page.videoExport),
163
+ ]);
164
+ const browser = report.browser || {};
165
+ section('browser', [
166
+ row('name', browser.name), row('platform', browser.platform), row('mobile', yes(browser.mobile)),
167
+ row('language', browser.language), row('screen', browser.screen), row('window', browser.window),
168
+ row('pixelRatio', browser.pixelRatio), row('cores', browser.cores), row('storage', browser.storage),
169
+ ]);
170
+ const gl = report.webgl || {};
171
+ section('graphics', [
172
+ ['WebGL', yes(gl.webgl1)], ['WebGL 2', yes(gl.webgl2)], row('renderer', gl.renderer),
173
+ row('largestTexture', gl.maxTexture), ['WebGPU', report.webgpu ? report.webgpu.state : t('doctor.unknown')],
174
+ ]);
175
+ const decode = report.decode || {};
176
+ section('decoding', [row('worker', decode.worker), row('lastDecoder', decode.last || t('doctor.noneYet'))]);
177
+ const audio = report.audio || {};
178
+ section('sound', [
179
+ row('muted', yes(audio.muted)), row('volume', audio.volume), row('audioContext', audio.context),
180
+ ]);
181
+ const server = report.server;
182
+ if (!server || server.error) {
183
+ lines.push(t('doctor.section.server'), ` ${server && server.error ? server.error : t('doctor.server.notAsked')}`, '');
184
+ return lines.join('\n').trimEnd();
185
+ }
186
+ const versions = server.versions || {};
187
+ section('versions', Object.entries(versions).map(([key, value]) => [key, value === null ? t('doctor.notInstalled') : value]));
188
+ const home = server.home || {};
189
+ section('home', [
190
+ row('path', home.path), row('exists', yes(home.exists)), row('writable', yes(home.writable)), row('freeSpace', size(home.free_bytes)),
191
+ ]);
192
+ const folders = Object.entries(server.folders || {});
193
+ if (folders.length) {
194
+ section('folders', folders.map(([name, entry]) => [name, t('doctor.folder', { count: entry.files, size: size(entry.bytes) })]));
195
+ }
196
+ section('library', Object.entries(server.library || {}).map(([key, value]) => [tOr(`doctor.library.${key}`, key.replaceAll('_', ' ')), value]));
197
+ lines.push(t('doctor.section.checks'));
198
+ for (const check of server.checks || []) {
199
+ lines.push(` ${!check.ok ? t('doctor.check.fail') : check.warning ? t('doctor.check.warn') : t('doctor.check.ok')} ${checkName(check)}, ${checkDetail(check)}`);
200
+ }
201
+ lines.push('', t('doctor.section.errors'));
202
+ const errors = server.errors || [];
203
+ if (!errors.length) {
204
+ lines.push(` ${t('doctor.none')}`);
205
+ }
206
+ for (const entry of errors) {
207
+ lines.push(` ${entry.at ? `${entry.at} ` : ''}${entry.level} ${entry.message}`);
208
+ }
209
+ return lines.join('\n').trimEnd();
210
+ }
211
+
212
+ // The browser's name and platform, from the client hints where the browser has them.
213
+ function browserFacts() {
214
+ const data = navigator.userAgentData;
215
+ const brands = data && Array.isArray(data.brands)
216
+ ? data.brands.filter((brand) => !/not.?a.?brand/i.test(brand.brand)).map((brand) => `${brand.brand} ${brand.version}`).join(', ')
217
+ : '';
218
+ return {
219
+ name: brands || navigator.userAgent,
220
+ platform: (data && data.platform) || navigator.platform || '',
221
+ mobile: !!(data && data.mobile),
222
+ language: navigator.language || '',
223
+ screen: t('doctor.by', { width: screen.width, height: screen.height }),
224
+ window: t('doctor.by', { width: window.innerWidth, height: window.innerHeight }),
225
+ pixelRatio: Math.round((window.devicePixelRatio || 1) * 100) / 100,
226
+ cores: navigator.hardwareConcurrency || null,
227
+ storage: null,
228
+ };
229
+ }
230
+
231
+ function webglFacts() {
232
+ const facts = { webgl1: false, webgl2: false, renderer: null, maxTexture: null };
233
+ for (const kind of ['webgl2', 'webgl']) {
234
+ let gl = null;
235
+ try {
236
+ gl = document.createElement('canvas').getContext(kind);
237
+ } catch {
238
+ gl = null;
239
+ }
240
+ if (!gl) {
241
+ continue;
242
+ }
243
+ facts[kind === 'webgl2' ? 'webgl2' : 'webgl1'] = true;
244
+ if (!facts.renderer) {
245
+ const info = gl.getExtension('WEBGL_debug_renderer_info');
246
+ facts.renderer = info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
247
+ facts.maxTexture = gl.getParameter(gl.MAX_TEXTURE_SIZE);
248
+ }
249
+ gl.getExtension('WEBGL_lose_context')?.loseContext();
250
+ }
251
+ if (facts.webgl2) {
252
+ facts.webgl1 = true;
253
+ }
254
+ return facts;
255
+ }
256
+
257
+ // Where Export video makes its videos, in this browser with the container and codec of its best
258
+ // encoder, or through the server when this browser cannot.
259
+ async function videoExportFacts() {
260
+ try {
261
+ const { browserSupport } = await import('../export/video-browser.js');
262
+ const support = await browserSupport();
263
+ if (support.supported) {
264
+ const codec = support.codec.startsWith('avc1') ? 'H.264' : support.codec.startsWith('vp09') ? 'VP9' : 'VP8';
265
+ return t('doctor.video.browser', { container: support.container === 'mp4' ? 'MP4' : 'WebM', codec });
266
+ }
267
+ return t(support.reason === 'pictures' ? 'doctor.video.pictures' : 'doctor.video.server');
268
+ } catch {
269
+ return t('doctor.video.server');
270
+ }
271
+ }
272
+
273
+ async function webgpuFacts() {
274
+ if (!navigator.gpu || typeof navigator.gpu.requestAdapter !== 'function') {
275
+ return { state: t('doctor.webgpu.none') };
276
+ }
277
+ try {
278
+ const adapter = await Promise.race([
279
+ navigator.gpu.requestAdapter(),
280
+ new Promise((resolve) => setTimeout(() => resolve('late'), 2000)),
281
+ ]);
282
+ if (adapter === 'late') {
283
+ return { state: t('doctor.webgpu.late') };
284
+ }
285
+ if (!adapter) {
286
+ return { state: t('doctor.webgpu.noAdapter') };
287
+ }
288
+ const info = adapter.info || {};
289
+ const named = [info.vendor, info.architecture].filter(Boolean).join(' ');
290
+ return { state: named ? t('doctor.webgpu.named', { adapter: named }) : t('doctor.webgpu.available') };
291
+ } catch (err) {
292
+ return { state: t('doctor.webgpu.failed', { error: err.message }) };
293
+ }
294
+ }
295
+
296
+ // Starts the decode worker and sends it three bytes, which it answers with a format error, so the
297
+ // worker shows it runs without reading a recording.
298
+ function workerFacts() {
299
+ if (typeof Worker !== 'function') {
300
+ return Promise.resolve(t('doctor.worker.none'));
301
+ }
302
+ return new Promise((resolve) => {
303
+ let worker;
304
+ const timer = setTimeout(() => {
305
+ worker?.terminate();
306
+ resolve(t('doctor.worker.late'));
307
+ }, 3000);
308
+ try {
309
+ worker = new Worker(new URL('../recording/decode-worker.js', import.meta.url), { type: 'module' });
310
+ } catch (err) {
311
+ clearTimeout(timer);
312
+ resolve(t('doctor.worker.start', { error: err.message }));
313
+ return;
314
+ }
315
+ worker.onmessage = (event) => {
316
+ // The worker first says that it started, and its answer to the bytes follows.
317
+ if (event.data && event.data.started) {
318
+ return;
319
+ }
320
+ clearTimeout(timer);
321
+ worker.terminate();
322
+ resolve(event.data && event.data.format ? t('doctor.worker.runs') : t('doctor.worker.odd'));
323
+ };
324
+ worker.onerror = (event) => {
325
+ clearTimeout(timer);
326
+ worker.terminate();
327
+ event.preventDefault?.();
328
+ resolve(t('doctor.worker.failed', { error: (event && event.message) || t('doctor.worker.notLoaded') }));
329
+ };
330
+ worker.postMessage({ id: -1, bytes: new Uint8Array([1, 2, 3]) });
331
+ });
332
+ }
333
+
334
+ async function getJson(path) {
335
+ const res = await fetch(path, { cache: 'no-store' });
336
+ if (!res.ok) {
337
+ throw new Error(t('doctor.error.status', { status: res.status }));
338
+ }
339
+ return res.json();
340
+ }
341
+
342
+ export class DoctorSection {
343
+ constructor(app, host) {
344
+ this.app = app;
345
+ this.host = host;
346
+ this.report = null;
347
+ this.makeButton = h('button', { type: 'button', class: 'btn primary', onclick: () => this.make() }, icon('file'), t('doctor.make'));
348
+ this.copyButton = h('button', { type: 'button', class: 'btn', disabled: true, ...symbolOnly(t('doctor.copy')), onclick: () => this.copy() }, icon('copy'));
349
+ this.saveButton = h('button', { type: 'button', class: 'btn', disabled: true, ...symbolOnly(t('doctor.save')), onclick: () => this.save() }, icon('download'));
350
+ this.status = h('p', { class: 'muted small', role: 'status', 'aria-live': 'polite' });
351
+ this.output = h('textarea', {
352
+ id: 'doctor-text', class: 'doctor-text', readonly: true, rows: '14', spellcheck: 'false', hidden: true,
353
+ 'aria-label': t('doctor.title'),
354
+ });
355
+ host.replaceChildren(
356
+ h('h2', { id: 'help-doctor-title' }, t('doctor.title')),
357
+ h('p', {}, tn('doctor.text', { command: h('code', {}, 'cuju render doctor') })),
358
+ h('div', { class: 'help-row' }, this.makeButton, this.copyButton, this.saveButton),
359
+ this.status,
360
+ this.output);
361
+ if (!document.getElementById('cuju-doctor-style')) {
362
+ const style = document.createElement('style');
363
+ style.id = 'cuju-doctor-style';
364
+ style.textContent = `
365
+ #help-doctor { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
366
+ .doctor-text { width: 100%; box-sizing: border-box; font-family: ui-monospace, Consolas, monospace; font-size: 12px;
367
+ background: var(--surface-2); color: var(--text); border: 1px solid var(--border); padding: 8px; resize: vertical; }
368
+ .doctor-text[hidden] { display: none; }
369
+ `;
370
+ document.head.appendChild(style);
371
+ }
372
+ }
373
+
374
+ // Everything the report holds, the page's side and the server's.
375
+ async gather() {
376
+ const app = this.app;
377
+ const report = { created: new Date().toISOString().slice(0, 19), page: {}, browser: browserFacts() };
378
+ let version = null;
379
+ try {
380
+ version = (await getJson('/api/info')).version;
381
+ } catch {
382
+ version = null;
383
+ }
384
+ const worker = navigator.serviceWorker && navigator.serviceWorker.controller;
385
+ const theme = document.getElementById('theme');
386
+ report.page = {
387
+ version,
388
+ origin: location.origin,
389
+ page: app.page,
390
+ drawing: app.noWebGL ? t('doctor.drawing.noWebGL') : (app.flat && app.flat.wanted ? t('doctor.drawing.flat') : '3D'),
391
+ reducedMotion: !!app.reducedMotion,
392
+ theme: theme ? theme.value : null,
393
+ standalone: !!(window.matchMedia && window.matchMedia('(display-mode: standalone)').matches),
394
+ serviceWorker: worker ? t('doctor.serviceWorker', { path: `${new URL(worker.scriptURL).pathname}${new URL(worker.scriptURL).search}` }) : t('doctor.none'),
395
+ };
396
+ report.page.videoExport = await videoExportFacts();
397
+ report.webgl = webglFacts();
398
+ const [gpu, decoder, estimate] = await Promise.all([
399
+ webgpuFacts(),
400
+ workerFacts(),
401
+ navigator.storage && navigator.storage.estimate ? navigator.storage.estimate().catch(() => null) : Promise.resolve(null),
402
+ ]);
403
+ report.webgpu = gpu;
404
+ report.decode = { worker: decoder, last: lastDecoder };
405
+ if (estimate && typeof estimate.usage === 'number') {
406
+ report.browser.storage = t('doctor.storage', { used: size(estimate.usage), quota: size(estimate.quota) });
407
+ }
408
+ const sound = app.sound;
409
+ report.audio = {
410
+ muted: !!(app.settings && app.settings.muted),
411
+ volume: app.settings ? app.settings.volume : null,
412
+ context: sound && sound.ctx ? sound.ctx.state : t('doctor.audio.notStarted'),
413
+ };
414
+ try {
415
+ report.server = await getJson('/api/help/doctor');
416
+ } catch (err) {
417
+ report.server = { error: t('doctor.server.failed', { error: err.message }) };
418
+ }
419
+ this.report = report;
420
+ return report;
421
+ }
422
+
423
+ async make() {
424
+ this.makeButton.disabled = true;
425
+ this.status.textContent = t('doctor.status.making');
426
+ try {
427
+ const report = await this.gather();
428
+ this.output.value = reportText(report);
429
+ this.output.hidden = false;
430
+ this.copyButton.disabled = false;
431
+ this.saveButton.disabled = false;
432
+ this.status.textContent = reportStatus(report);
433
+ return report;
434
+ } catch (err) {
435
+ this.status.textContent = t('doctor.status.failed', { error: err.message });
436
+ throw err;
437
+ } finally {
438
+ this.makeButton.disabled = false;
439
+ }
440
+ }
441
+
442
+ async text() {
443
+ if (!this.report) {
444
+ await this.make();
445
+ }
446
+ return reportText(this.report);
447
+ }
448
+
449
+ async copy() {
450
+ const text = await this.text();
451
+ try {
452
+ await navigator.clipboard.writeText(text);
453
+ this.status.textContent = t('doctor.status.copied');
454
+ return true;
455
+ } catch {
456
+ // Without the clipboard, the text is selected for the person to copy.
457
+ this.output.hidden = false;
458
+ this.output.focus();
459
+ this.output.select();
460
+ this.status.textContent = t('doctor.status.selected');
461
+ return false;
462
+ }
463
+ }
464
+
465
+ async save() {
466
+ const text = await this.text();
467
+ const name = `cuju-report-${new Date().toISOString().slice(0, 10)}.txt`;
468
+ const url = URL.createObjectURL(new Blob([`${text}\n`], { type: 'text/plain' }));
469
+ const link = h('a', { href: url, download: name, hidden: true });
470
+ document.body.append(link);
471
+ this.lastSave = { name, bytes: text.length + 1 };
472
+ link.click();
473
+ link.remove();
474
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
475
+ this.status.textContent = t('doctor.status.saved', { name });
476
+ return name;
477
+ }
478
+
479
+ // The functions of window.cuju.doctor.
480
+ script() {
481
+ return {
482
+ gather: () => this.make(),
483
+ text: () => this.text(),
484
+ copy: () => this.copy(),
485
+ save: () => this.save(),
486
+ };
487
+ }
488
+ }