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,257 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The installable page. On the loopback address of the server, in a window of its own rather than
5
+ // a notebook's frame, the page registers its service worker, sw.js, with the build of the page
6
+ // that the server names, and Chrome and Edge then offer to install it as an app. The Help page
7
+ // shows whether the page is installed and offers Install where the browser allows it, Update once
8
+ // the server holds a newer build, and the removal of the copy that the worker keeps. Safari on a
9
+ // Mac adds the page to the Dock from its File menu, which Help then names, and every browser
10
+ // reaches an app window through the command cuju render app, which Help names as well. The
11
+ // browser's offer comes from install-state.js, which the card on Home shares. Scripts reach it as
12
+ // window.cuju.install.
13
+
14
+ import { t, tn } from '../core/i18n.js';
15
+ import { icon } from '../core/icons.js';
16
+ import { heldPrompt, installRoute, installedNow, onInstallChange, promptInstall, runsAsApp } from './install-state.js';
17
+
18
+ const LOOPBACK = ['127.0.0.1', 'localhost', '[::1]'];
19
+ const PREFIX = 'cuju-page-';
20
+
21
+ function h(tag, attributes = {}, ...children) {
22
+ const node = document.createElement(tag);
23
+ for (const [key, value] of Object.entries(attributes)) {
24
+ if (key.startsWith('on') && typeof value === 'function') {
25
+ node.addEventListener(key.slice(2), value);
26
+ } else if (value !== null && value !== undefined && value !== false) {
27
+ node.setAttribute(key, value === true ? '' : value);
28
+ }
29
+ }
30
+ node.append(...children.flat().filter((child) => child !== null && child !== undefined));
31
+ return node;
32
+ }
33
+
34
+ // Whether this page may register its worker, on http at a loopback host and at the top.
35
+ export function installable(where = location, framed = null) {
36
+ let top = framed;
37
+ if (top === null) {
38
+ try {
39
+ top = window.top !== window;
40
+ } catch {
41
+ top = true;
42
+ }
43
+ }
44
+ return where.protocol === 'http:' && LOOPBACK.includes(where.hostname) && !top
45
+ && typeof navigator !== 'undefined' && 'serviceWorker' in navigator;
46
+ }
47
+
48
+ // The build of a worker, from its address.
49
+ export function workerBuild(scriptURL) {
50
+ try {
51
+ return new URL(scriptURL).searchParams.get('build');
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ async function serverBuild() {
58
+ const res = await fetch('/api/help/app', { cache: 'no-store' });
59
+ if (!res.ok) {
60
+ throw new Error(t('pwa.error.status', { status: res.status }));
61
+ }
62
+ return (await res.json()).build;
63
+ }
64
+
65
+ export class AppInstall {
66
+ constructor(app, host) {
67
+ this.app = app;
68
+ this.host = host;
69
+ this.supported = installable();
70
+ this.registration = null;
71
+ this.build = null;
72
+ this.latest = null;
73
+ this.error = null;
74
+ this.route = installRoute({
75
+ hasEvent: 'onbeforeinstallprompt' in window, userAgent: navigator.userAgent, vendor: navigator.vendor,
76
+ });
77
+ this.status = h('p', { class: 'muted small', role: 'status', 'aria-live': 'polite' });
78
+ this.installButton = h('button', { type: 'button', class: 'btn primary', hidden: true, onclick: () => this.install() }, icon('download'), t('pwa.install'));
79
+ this.updateButton = h('button', { type: 'button', class: 'btn', hidden: true, onclick: () => this.update(true) }, icon('refresh'), t('pwa.update'));
80
+ this.forgetButton = h('button', { type: 'button', class: 'btn', hidden: true, onclick: () => this.unregister() }, icon('trash'), t('pwa.remove'));
81
+ const code = (text) => h('code', {}, text);
82
+ host.replaceChildren(
83
+ h('h2', { id: 'help-install-title' }, t('pwa.title')),
84
+ h('p', {}, ...tn('pwa.text')),
85
+ h('div', { class: 'help-row' }, this.installButton, this.updateButton, this.forgetButton),
86
+ this.status,
87
+ // The command that opens cuju in a window of its own shows the symbol of a terminal.
88
+ h('p', { id: 'help-install-command' }, icon('terminal', 'icon text-symbol'), tn('pwa.command.text', {
89
+ command: code('cuju render app'), install: code('cuju render app --install'),
90
+ })));
91
+ onInstallChange(() => this.render());
92
+ if (this.supported) {
93
+ navigator.serviceWorker.addEventListener('controllerchange', () => this.render());
94
+ }
95
+ const page = document.getElementById('page-help');
96
+ if (page) {
97
+ new MutationObserver(() => {
98
+ if (!page.hidden) {
99
+ this.check();
100
+ }
101
+ }).observe(page, { attributes: true, attributeFilter: ['hidden'] });
102
+ }
103
+ this.ready = this.register();
104
+ }
105
+
106
+ async register() {
107
+ if (!this.supported) {
108
+ this.render();
109
+ return this.state();
110
+ }
111
+ try {
112
+ this.build = await serverBuild();
113
+ this.latest = this.build;
114
+ this.registration = await navigator.serviceWorker.register(`sw.js?build=${encodeURIComponent(this.build)}`, {
115
+ scope: '../', updateViaCache: 'none',
116
+ });
117
+ this.registration.addEventListener('updatefound', () => this.render());
118
+ this.error = null;
119
+ } catch (err) {
120
+ this.error = err.message;
121
+ }
122
+ this.render();
123
+ return this.state();
124
+ }
125
+
126
+ // Asks the server for its build, which differs from the page's once a new version is there.
127
+ async check() {
128
+ if (!this.supported) {
129
+ return this.state();
130
+ }
131
+ try {
132
+ this.latest = await serverBuild();
133
+ } catch {
134
+ // The server is away, the page stays as it is.
135
+ }
136
+ this.render();
137
+ return this.state();
138
+ }
139
+
140
+ async install() {
141
+ const result = await promptInstall();
142
+ this.render();
143
+ if (result.outcome === 'unavailable') {
144
+ this.status.textContent = t(this.route === 'browser' ? 'pwa.unavailable' : `pwa.route.${this.route}`);
145
+ }
146
+ return result;
147
+ }
148
+
149
+ // Takes the newest build, the worker of the server's build, and with reload shows it at once.
150
+ async update(reload = false) {
151
+ await this.check();
152
+ if (this.supported && this.latest && this.latest !== this.build) {
153
+ this.build = this.latest;
154
+ this.registration = await navigator.serviceWorker.register(`sw.js?build=${encodeURIComponent(this.build)}`, {
155
+ scope: '../', updateViaCache: 'none',
156
+ });
157
+ if (reload) {
158
+ location.reload();
159
+ }
160
+ }
161
+ this.render();
162
+ return this.state();
163
+ }
164
+
165
+ // Removes the worker and the copy it keeps, until the page registers it again on its next load.
166
+ async unregister() {
167
+ let removed = false;
168
+ if (this.supported) {
169
+ for (const registration of await navigator.serviceWorker.getRegistrations()) {
170
+ removed = (await registration.unregister()) || removed;
171
+ }
172
+ }
173
+ if (typeof caches !== 'undefined') {
174
+ for (const name of await caches.keys()) {
175
+ if (name.startsWith(PREFIX)) {
176
+ await caches.delete(name);
177
+ }
178
+ }
179
+ }
180
+ this.registration = null;
181
+ this.render();
182
+ this.status.textContent = t('pwa.removed');
183
+ return removed;
184
+ }
185
+
186
+ state() {
187
+ const controller = this.supported ? navigator.serviceWorker.controller : null;
188
+ const standalone = runsAsApp();
189
+ return {
190
+ supported: this.supported,
191
+ registered: !!this.registration,
192
+ build: this.build,
193
+ latest: this.latest,
194
+ controlled: !!controller,
195
+ controllerBuild: controller ? workerBuild(controller.scriptURL) : null,
196
+ waiting: !!(this.registration && (this.registration.waiting || this.registration.installing)),
197
+ updateReady: !!(this.latest && this.build && this.latest !== this.build),
198
+ installable: !!heldPrompt(),
199
+ installed: installedNow(),
200
+ standalone,
201
+ route: this.route,
202
+ error: this.error,
203
+ };
204
+ }
205
+
206
+ render() {
207
+ const state = this.state();
208
+ this.installButton.hidden = !state.installable || state.standalone;
209
+ this.updateButton.hidden = !state.updateReady;
210
+ this.forgetButton.hidden = !state.registered;
211
+ // The line opens with the symbol of its state where one reads clearly, a ticked circle for
212
+ // an installed app, a warning for an error and a terminal for the command.
213
+ let text;
214
+ let symbol = null;
215
+ if (!this.supported) {
216
+ text = tn('pwa.state.unsupported');
217
+ } else if (state.error) {
218
+ text = tn('pwa.state.error', { error: state.error });
219
+ symbol = 'warning';
220
+ } else if (state.standalone) {
221
+ text = tn('pwa.state.standalone');
222
+ symbol = 'done';
223
+ } else if (state.installable) {
224
+ text = tn('pwa.state.installable');
225
+ } else if (state.installed) {
226
+ text = tn('pwa.state.installed');
227
+ symbol = 'done';
228
+ } else if (this.route !== 'browser') {
229
+ // Safari names its own menu, and another browser the command that makes the app.
230
+ text = tn(`pwa.route.${this.route}`);
231
+ symbol = this.route === 'command' ? 'terminal' : null;
232
+ } else if (state.controlled) {
233
+ text = tn('pwa.state.keptMenu');
234
+ } else {
235
+ text = tn('pwa.state.menu');
236
+ }
237
+ // A second sentence follows the first, as its own message, and each keeps its marks.
238
+ if (state.updateReady) {
239
+ text.push(' ', ...tn('pwa.state.updateReady'));
240
+ } else if (state.controlled && (state.standalone || state.installable || state.installed || this.route !== 'browser')) {
241
+ text.push(' ', ...tn('pwa.state.kept'));
242
+ }
243
+ this.status.replaceChildren(...(symbol ? [icon(symbol, 'icon text-symbol')] : []), ...text);
244
+ }
245
+
246
+ // The functions of window.cuju.install.
247
+ script() {
248
+ return {
249
+ state: () => this.state(),
250
+ install: () => this.install(),
251
+ update: () => this.update(false),
252
+ check: () => this.check(),
253
+ unregister: () => this.unregister(),
254
+ ready: () => this.ready,
255
+ };
256
+ }
257
+ }
@@ -0,0 +1,241 @@
1
+ // Copyright 2026 The cuju Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ // The steps of the guided tour and the rules that move through them. Each step names a page, the
5
+ // control it points at as a selector, a title and a line of text, and what it needs to show, a
6
+ // recording that plays or a team of the registry, which the tour offers to add first, and what
7
+ // its Try it button does, with the reason written beside each step. The browser keeps the step
8
+ // reached, so a tour left midway resumes there. Nothing here touches the page, so
9
+ // the rules hold the same in a test. The title and text of a step are the messages
10
+ // tour.step.<id>.title and tour.step.<id>.text.
11
+
12
+ import { t } from '../core/i18n.js';
13
+
14
+ export const TOUR_KEY = 'cuju.tour';
15
+
16
+ export const TOUR_STEPS = [
17
+ {
18
+ // None, the logo leads to Home, the page this step already shows.
19
+ id: 'home', page: 'home', target: '#home-btn', title: t('tour.step.home.title'),
20
+ text: t('tour.step.home.text'), tryIt: null,
21
+ },
22
+ {
23
+ // Open, the button opens the command palette, which takes the focus to its search field.
24
+ id: 'palette', page: 'home', target: '#open-palette', title: t('tour.step.palette.title'),
25
+ text: t('tour.step.palette.text'), tryIt: 'open',
26
+ },
27
+ {
28
+ // Open, the button opens the Match menu, and the focus moves to its first item.
29
+ id: 'match', page: 'watch', target: '#match-menu-btn', title: t('tour.step.match.title'),
30
+ text: t('tour.step.match.text'), tryIt: 'open',
31
+ needs: 'recording',
32
+ },
33
+ {
34
+ // Focus, the timeline is a slider moved by keys, so the focus moves to it and the keys
35
+ // that seek are read out.
36
+ id: 'timeline', page: 'watch', target: '#timeline', title: t('tour.step.timeline.title'),
37
+ text: t('tour.step.timeline.text'), tryIt: 'focus',
38
+ needs: 'recording',
39
+ },
40
+ {
41
+ // Open, the button opens the View menu, and the focus moves to its first item.
42
+ id: 'view', page: 'watch', target: '#view-menu-btn', title: t('tour.step.view.title'),
43
+ text: t('tour.step.view.text'), tryIt: 'open',
44
+ needs: 'recording',
45
+ },
46
+ {
47
+ // None, the Play button starts a match, which leaves the tour behind, and the setup it
48
+ // belongs to already shows on this page.
49
+ id: 'play', page: 'play', target: '.play-setup .play-row .btn.primary', title: t('tour.step.play.title'),
50
+ text: t('tour.step.play.text'), tryIt: null,
51
+ },
52
+ {
53
+ // Open, the Leaderboard button of the first challenge opens its leaderboard on the card,
54
+ // which the same button closes again. Play it yourself and Run a model beside it start
55
+ // matches, so the step points past them.
56
+ id: 'challenges', page: 'challenges', target: '#page-challenges .challenge-item [data-action="board"]',
57
+ title: t('tour.step.challenges.title'), text: t('tour.step.challenges.text'), tryIt: 'open',
58
+ },
59
+ {
60
+ // None, the target is the list of teams, and adding a team uploads a folder.
61
+ id: 'teams', page: 'teams', target: '#teams-side', title: t('tour.step.teams.title'),
62
+ text: t('tour.step.teams.text'), tryIt: null,
63
+ needs: 'team',
64
+ },
65
+ {
66
+ // Open, New tournament opens the wizard beside the list on this page, a draft that
67
+ // nothing saves before its last step and that Cancel puts away.
68
+ id: 'tournaments', page: 'tournaments', target: '.cup-side-head .btn', title: t('tour.step.tournaments.title'),
69
+ text: t('tour.step.tournaments.text'), tryIt: 'open',
70
+ },
71
+ {
72
+ // Open, the button opens the settings panel, and the focus moves to its first field.
73
+ id: 'settings', page: 'home', target: '#open-settings', title: t('tour.step.settings.title'),
74
+ text: t('tour.step.settings.text'), tryIt: 'open',
75
+ },
76
+ {
77
+ // None, the target is the heading of the Learn by doing part, and its lessons start
78
+ // tasks of their own.
79
+ id: 'help', page: 'help', target: '#help-learn-title', title: t('tour.step.help.title'),
80
+ text: t('tour.step.help.text'), tryIt: null,
81
+ },
82
+ ];
83
+
84
+ // What Try it does on a step. Open clicks the control as a person would, so whatever it opens
85
+ // behaves as usual and keeps the focus where it puts it, and focus moves the focus to a control
86
+ // used by keys and says which keys move it, the message tour.try.<id>. A step with neither shows
87
+ // no Try it.
88
+ export const TRY_KINDS = ['open', 'focus'];
89
+
90
+ // The offer of a step whose need is missing.
91
+ export const NEED_OFFERS = {
92
+ recording: { label: t('tour.need.recording.label'), text: t('tour.need.recording.text') },
93
+ team: { label: t('tour.need.team.label'), text: t('tour.need.team.text') },
94
+ };
95
+
96
+ // The stored state of the tour, the step reached and whether the tour ended, kept in range.
97
+ export function tourState(stored, count = TOUR_STEPS.length) {
98
+ const source = stored && typeof stored === 'object' ? stored : {};
99
+ const step = Number.isInteger(source.step) ? Math.max(0, Math.min(count - 1, source.step)) : 0;
100
+ return { step, done: source.done === true, started: source.started === true };
101
+ }
102
+
103
+ // The state after moving by delta steps. Moving past the last step ends the tour, moving before
104
+ // the first stays there.
105
+ export function moveTour(state, delta, count = TOUR_STEPS.length) {
106
+ const clean = tourState(state, count);
107
+ const next = clean.step + delta;
108
+ if (next >= count) {
109
+ return { step: 0, done: true, started: false };
110
+ }
111
+ return { step: Math.max(0, next), done: false, started: true };
112
+ }
113
+
114
+ // The step where the tour starts, the one reached when a tour was left midway.
115
+ export function resumeStep(state, count = TOUR_STEPS.length) {
116
+ const clean = tourState(state, count);
117
+ return clean.started && !clean.done ? clean.step : 0;
118
+ }
119
+
120
+ // Whether a step can show as it stands, given what the page holds.
121
+ export function needMet(step, facts) {
122
+ if (!step || !step.needs) {
123
+ return true;
124
+ }
125
+ return !!(facts && facts[step.needs]);
126
+ }
127
+
128
+ // The words a screen reader hears for a step.
129
+ export function stepAnnouncement(step, index, count = TOUR_STEPS.length, found = true) {
130
+ return t(found ? 'tour.announce' : 'tour.announce.missing', { index: index + 1, count, title: step.title, text: step.text });
131
+ }
132
+
133
+ // The edges of a box given by its left and top and either its right and bottom or its size.
134
+ function edges(box) {
135
+ const right = Number.isFinite(box.right) ? box.right : box.left + (box.width || 0);
136
+ const bottom = Number.isFinite(box.bottom) ? box.bottom : box.top + (box.height || 0);
137
+ return { left: box.left, top: box.top, right, bottom };
138
+ }
139
+
140
+ // Whether two boxes share some area, and boxes that only touch do not.
141
+ const overlaps = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
142
+
143
+ // The distance between two boxes, zero when they overlap or touch.
144
+ const apart = (a, b) => Math.hypot(Math.max(0, b.left - a.right, a.left - b.right), Math.max(0, b.top - a.bottom, a.top - b.bottom));
145
+
146
+ // The smallest the card becomes when it docks in a corner.
147
+ const DOCK_WIDTH = 240;
148
+ const DOCK_HEIGHT = 120;
149
+
150
+ // The card docked in the corner of the window farthest from the boxes, at its full size when a
151
+ // corner has room for it, else made narrower or shorter until it clears them, or null when no
152
+ // corner has room for the smallest card.
153
+ function dock(boxes, width, height, view, margin) {
154
+ const minWidth = Math.min(width, DOCK_WIDTH);
155
+ const minHeight = Math.min(height, DOCK_HEIGHT);
156
+ const at = (right, bottom, w, h) => {
157
+ const left = right ? view.width - margin - w : margin;
158
+ const top = bottom ? view.height - margin - h : margin;
159
+ return { left, top, right: left + w, bottom: top + h };
160
+ };
161
+ let best = null;
162
+ for (const [right, bottom] of [[false, false], [true, false], [false, true], [true, true]]) {
163
+ let w = width;
164
+ let h = height;
165
+ let clear = false;
166
+ for (let pass = 0; pass <= boxes.length; pass++) {
167
+ const hit = boxes.find((box) => overlaps(at(right, bottom, w, h), box));
168
+ if (!hit) {
169
+ clear = true;
170
+ break;
171
+ }
172
+ // The card keeps clear of the box by its width or by its height, whichever keeps more
173
+ // of the card.
174
+ const freeWidth = Math.floor(right ? view.width - margin - (hit.right + margin) : hit.left - 2 * margin);
175
+ const freeHeight = Math.floor(bottom ? view.height - margin - (hit.bottom + margin) : hit.top - 2 * margin);
176
+ const byWidth = freeWidth >= minWidth ? Math.min(w, freeWidth) * h : -1;
177
+ const byHeight = freeHeight >= minHeight ? w * Math.min(h, freeHeight) : -1;
178
+ if (byWidth < 0 && byHeight < 0) {
179
+ break;
180
+ }
181
+ if (byWidth >= byHeight) {
182
+ w = Math.min(w, freeWidth);
183
+ } else {
184
+ h = Math.min(h, freeHeight);
185
+ }
186
+ }
187
+ if (!clear) {
188
+ continue;
189
+ }
190
+ const spot = at(right, bottom, w, h);
191
+ const area = w * h;
192
+ const distance = Math.min(...boxes.map((box) => apart(spot, box)));
193
+ if (!best || area > best.area || (area === best.area && distance > best.distance)) {
194
+ best = { spot, w, h, area, distance };
195
+ }
196
+ }
197
+ return best ? {
198
+ left: Math.round(best.spot.left), top: Math.round(best.spot.top), width: best.w, height: best.h, side: 'dock',
199
+ } : null;
200
+ }
201
+
202
+ // Where the card of a step goes beside its target, below it when there is room, above it when
203
+ // not, then to its right or left, each only where the card covers neither the target nor any of
204
+ // the boxes to avoid, such as a menu or a panel open over the page. When no side is clear the
205
+ // card docks in the corner of the window farthest from them, smaller when it must, and when no
206
+ // corner is clear either it goes over the target at the bottom of the window. Without a target
207
+ // the card goes to the bottom of the window, or docks when a box covers that spot. Every value is
208
+ // a pixel position of the window, inside its margins, and a docked card also has its width and
209
+ // height.
210
+ export function placeCard(target, card, view, avoid = [], margin = 12) {
211
+ const width = Math.min(card.width, view.width - 2 * margin);
212
+ const boxes = [target, ...(avoid || [])].filter(Boolean).map(edges);
213
+ const clear = (left, top) => !boxes.some((box) => overlaps({ left, top, right: left + width, bottom: top + card.height }, box));
214
+ const docked = () => dock(boxes, width, Math.min(card.height, view.height - 2 * margin), view, margin);
215
+ if (!target) {
216
+ const spot = { left: Math.round((view.width - width) / 2), top: Math.round(view.height - card.height - margin), side: 'none' };
217
+ return clear(spot.left, spot.top) ? spot : docked() || spot;
218
+ }
219
+ const box = edges(target);
220
+ const left = Math.round(Math.max(margin, Math.min(view.width - width - margin, box.left + (box.right - box.left) / 2 - width / 2)));
221
+ const sides = [];
222
+ if (box.bottom + margin + card.height <= view.height - margin) {
223
+ sides.push({ left, top: Math.round(box.bottom + margin), side: 'below' });
224
+ }
225
+ if (box.top - margin - card.height >= margin) {
226
+ sides.push({ left, top: Math.round(box.top - margin - card.height), side: 'above' });
227
+ }
228
+ // A tall target leaves room at its side more often than above or below it.
229
+ const top = Math.round(Math.max(margin, Math.min(view.height - card.height - margin, box.top)));
230
+ if (box.right + margin + width <= view.width - margin) {
231
+ sides.push({ left: Math.round(box.right + margin), top, side: 'right' });
232
+ }
233
+ if (box.left - margin - width >= margin) {
234
+ sides.push({ left: Math.round(box.left - margin - width), top, side: 'left' });
235
+ }
236
+ const side = sides.find((spot) => clear(spot.left, spot.top));
237
+ if (side) {
238
+ return side;
239
+ }
240
+ return docked() || { left, top: Math.round(Math.max(margin, view.height - card.height - margin)), side: 'over' };
241
+ }