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,3 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """The team registry, the celebration clips and the joints of its models."""
@@ -0,0 +1,394 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The celebration clips that a team uploads, animations on a skinned skeleton in binary glTF.
5
+
6
+ A clip is a binary glTF 2.0 file, `.glb`, with a skinned skeleton and one or more animations. The
7
+ page turns each animation into poses of GRF's skeleton by a joint mapping, as it does for a custom
8
+ model, so any model of a team plays it after a goal. The files live in the folder `clips` of the
9
+ team's `cuju-team` folder, each beside a small JSON file with its joint mapping and the names of its
10
+ animations, and travel with the team when it is exported.
11
+
12
+ Every file is checked as it arrives and again when an exported team comes back. A clip holds at
13
+ most 10 MB, a team at most 16 clips and 40 MB of them, and a name of the page's own making, so no
14
+ request reaches outside the team's folder.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import contextlib
20
+ import json
21
+ import re
22
+ import struct
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from cuju.errors import ArgumentError
27
+
28
+ from .._text.messages import MessageError
29
+ from . import teams as _teams
30
+
31
+ FOLDER = 'clips'
32
+ MAXIMUM_CLIP_BYTES = 10 * 1024 * 1024
33
+ MAXIMUM_TEAM_BYTES = 40 * 1024 * 1024
34
+ MAXIMUM_CLIPS = 16
35
+ MAXIMUM_ANIMATIONS = 32
36
+ # A clip's name, which the page makes from the file's name, is its file's name too.
37
+ _NAME = re.compile(r'[a-z0-9_]{1,40}')
38
+ # glTF: the chunk types of a binary file, its JSON and its binary buffer.
39
+ _JSON_CHUNK = 0x4E4F534A
40
+ _BIN_CHUNK = 0x004E4942
41
+ # Extensions whose decoders the page leaves out, so a file that requires one cannot load there.
42
+ _UNSUPPORTED = ('KHR_draco_mesh_compression', 'EXT_meshopt_compression', 'KHR_texture_basisu')
43
+
44
+
45
+ def check_name(name: Any) -> str:
46
+ """Returns the name of a clip, or raises `ArgumentError` for one that is no safe file name."""
47
+ if not isinstance(name, str) or not _NAME.fullmatch(name) or _teams._device(name):
48
+ raise MessageError('server.clips.name', name=repr(name))
49
+ return name
50
+
51
+
52
+ def _uris(value: Any) -> list[str]:
53
+ """Returns every `uri` string of a glTF document that names something outside the file."""
54
+ found = []
55
+ if isinstance(value, dict):
56
+ for key, item in value.items():
57
+ if key == 'uri' and isinstance(item, str) and not item.startswith('data:'):
58
+ found.append(item)
59
+ else:
60
+ found.extend(_uris(item))
61
+ elif isinstance(value, list):
62
+ for item in value:
63
+ found.extend(_uris(item))
64
+ return found
65
+
66
+
67
+ def check(data: bytes) -> dict[str, Any]:
68
+ """Checks the bytes of a clip and returns the names of its animations and skinned bones.
69
+
70
+ Returns:
71
+ A dictionary with `animations`, the names of the animations in file order, and `bones`,
72
+ the names of the joints of every skin.
73
+
74
+ Raises:
75
+ ArgumentError: If the bytes are too many, are no binary glTF 2.0 file of sound chunks,
76
+ link to outside files, need a decoder that the page lacks, or hold no skin or no
77
+ animation.
78
+ """
79
+ if len(data) > MAXIMUM_CLIP_BYTES:
80
+ raise MessageError('server.clips.size', size=len(data))
81
+ # glTF: a binary file opens with `glTF`, the version and the length, then its chunks, each
82
+ # with its length and type, padded to four bytes, the JSON chunk first.
83
+ if len(data) < 20 or data[:4] != b'glTF':
84
+ raise MessageError('server.clips.binary')
85
+ version, length = struct.unpack_from('<II', data, 4)
86
+ if version != 2:
87
+ raise MessageError('server.clips.version', version=version)
88
+ if length != len(data):
89
+ raise MessageError('server.clips.length', length=length, size=len(data))
90
+ chunks = []
91
+ offset = 12
92
+ while offset < length:
93
+ if offset + 8 > length:
94
+ raise MessageError('server.clips.cut')
95
+ size, kind = struct.unpack_from('<II', data, offset)
96
+ if size % 4 or offset + 8 + size > length:
97
+ raise MessageError('server.clips.padding')
98
+ chunks.append((kind, offset + 8, size))
99
+ offset += 8 + size
100
+ if not chunks or chunks[0][0] != _JSON_CHUNK:
101
+ raise MessageError('server.clips.json_first')
102
+ if any(kind == _JSON_CHUNK for kind, _, _ in chunks[1:]) or (
103
+ sum(kind == _BIN_CHUNK for kind, _, _ in chunks) > 1
104
+ or any(kind == _BIN_CHUNK for kind, _, _ in chunks[2:])
105
+ ):
106
+ raise MessageError('server.clips.chunks')
107
+ _, start, size = chunks[0]
108
+ try:
109
+ document = json.loads(data[start : start + size])
110
+ outside = _uris(document)
111
+ except (ValueError, UnicodeDecodeError, RecursionError) as error:
112
+ raise MessageError('server.clips.json', error=error) from error
113
+ if not isinstance(document, dict):
114
+ raise MessageError('server.clips.json_object')
115
+ asset = document.get('asset')
116
+ if not isinstance(asset, dict) or not str(asset.get('version', '')).startswith('2.'):
117
+ raise MessageError('server.clips.asset_version')
118
+ if outside:
119
+ raise MessageError('server.clips.link', uri=repr(outside[0]))
120
+ binary = next((size for kind, _, size in chunks if kind == _BIN_CHUNK), None)
121
+ for buffer in _teams._array(document.get('buffers')):
122
+ if isinstance(buffer, dict) and 'uri' not in buffer:
123
+ if binary is None or not isinstance(buffer.get('byteLength'), int):
124
+ raise MessageError('server.clips.buffer')
125
+ if buffer['byteLength'] > binary:
126
+ raise MessageError('server.clips.buffer_length')
127
+ required = [
128
+ name for name in _teams._array(document.get('extensionsRequired')) if name in _UNSUPPORTED
129
+ ]
130
+ if required:
131
+ raise MessageError('server.clips.extension', extension=required[0])
132
+ nodes = document.get('nodes') or []
133
+ skins = document.get('skins') or []
134
+ if not isinstance(nodes, list) or not isinstance(skins, list) or not skins:
135
+ raise MessageError('server.clips.skinned')
136
+ bones = set()
137
+ for skin in skins:
138
+ for joint in _teams._array(skin.get('joints')) if isinstance(skin, dict) else []:
139
+ if (
140
+ isinstance(joint, int)
141
+ and 0 <= joint < len(nodes)
142
+ and isinstance(nodes[joint], dict)
143
+ ):
144
+ bones.add(str(nodes[joint].get('name', '')))
145
+ if not bones:
146
+ raise MessageError('server.clips.skin_joints')
147
+ animations = document.get('animations') or []
148
+ if not isinstance(animations, list) or not animations:
149
+ raise MessageError('server.clips.animations_none')
150
+ if len(animations) > MAXIMUM_ANIMATIONS:
151
+ raise MessageError(
152
+ 'server.clips.animations_count', most=MAXIMUM_ANIMATIONS, count=len(animations)
153
+ )
154
+ names = []
155
+ for index, animation in enumerate(animations):
156
+ channels = animation.get('channels') if isinstance(animation, dict) else None
157
+ samplers = animation.get('samplers') if isinstance(animation, dict) else None
158
+ if not isinstance(channels, list) or not channels or not isinstance(samplers, list):
159
+ raise MessageError('server.clips.animation', index=index)
160
+ for channel in channels:
161
+ target = channel.get('target') if isinstance(channel, dict) else None
162
+ node = target.get('node') if isinstance(target, dict) else None
163
+ sampler = channel.get('sampler') if isinstance(channel, dict) else None
164
+ if not (
165
+ isinstance(node, int)
166
+ and 0 <= node < len(nodes)
167
+ and isinstance(sampler, int)
168
+ and 0 <= sampler < len(samplers)
169
+ ):
170
+ raise MessageError('server.clips.channels', index=index)
171
+ name = animation.get('name')
172
+ name = _teams._printable(name).strip()[:60] if isinstance(name, str) else ''
173
+ names.append(name or f'Animation {index + 1}')
174
+ return {'animations': names, 'bones': bones}
175
+
176
+
177
+ def _check_joints(joints: Any, bones: set[str]) -> dict[str, str]:
178
+ """Returns a joint mapping of a clip, empty for one that the page guesses from bone names."""
179
+ if joints is None:
180
+ return {}
181
+ if not isinstance(joints, dict) or not all(
182
+ isinstance(key, str) and isinstance(value, str) for key, value in joints.items()
183
+ ):
184
+ raise MessageError('server.clips.joints', joints=repr(joints))
185
+ if not joints:
186
+ return {}
187
+ if 'body' not in joints:
188
+ raise MessageError('server.clips.body')
189
+ unknown = [joint for joint in joints if joint not in (*_teams.JOINTS, 'player')]
190
+ if unknown:
191
+ raise MessageError('server.clips.unknown_joints', joints=', '.join(unknown))
192
+ absent = [bone for bone in joints.values() if bone not in bones]
193
+ if absent:
194
+ raise MessageError('server.clips.missing_bones', bones=', '.join(absent))
195
+ return dict(joints)
196
+
197
+
198
+ def _folder(identifier: str) -> Path:
199
+ """Returns the folder of a team's clips, without creating it."""
200
+ return _teams._meta_path(identifier).parent / FOLDER
201
+
202
+
203
+ def _sidecar(path: Path) -> Path:
204
+ """Returns the JSON file beside a clip, which keeps its mapping and animation names."""
205
+ return path.with_suffix('.json')
206
+
207
+
208
+ def _clip_files(folder: Path) -> list[Path]:
209
+ """Returns the clips of a folder of clips, the files named exactly as a clip, in name order.
210
+
211
+ Windows and macOS find a file whatever the case of its name, so the names come from the
212
+ folder's own listing, and a link, which could point outside the team's folder, is left out.
213
+ """
214
+ return sorted(
215
+ path
216
+ for name in _teams._names(folder)
217
+ if name.endswith('.glb') and _NAME.fullmatch(name[: -len('.glb')])
218
+ if (path := folder / name).is_file() and not path.is_symlink()
219
+ )
220
+
221
+
222
+ def listing(team_folder: Path) -> list[dict[str, Any]]:
223
+ """Returns the clips of a team's `cuju-team` folder, each its name, size, animations and joints.
224
+
225
+ The JSON file beside each clip, which the server wrote after checking the clip, gives the
226
+ animations and the joints. A clip whose JSON file is missing or damaged is checked again.
227
+ """
228
+ folder = team_folder / FOLDER
229
+ clips = []
230
+ for path in _clip_files(folder):
231
+ try:
232
+ meta = json.loads(_sidecar(path).read_text(encoding='utf-8'))
233
+ animations = meta['animations']
234
+ joints = meta['joints']
235
+ if not (isinstance(animations, list) and isinstance(joints, dict)):
236
+ raise ValueError('expected the animations and joints of a clip')
237
+ except (OSError, ValueError, KeyError, TypeError):
238
+ try:
239
+ found = check(path.read_bytes())
240
+ except (OSError, ArgumentError):
241
+ continue
242
+ animations, joints = found['animations'], {}
243
+ with contextlib.suppress(OSError):
244
+ _write_sidecar(path, animations, joints)
245
+ clips.append(
246
+ {
247
+ 'name': path.stem,
248
+ 'bytes': path.stat().st_size,
249
+ 'animations': animations,
250
+ 'joints': joints,
251
+ }
252
+ )
253
+ return clips
254
+
255
+
256
+ def _write_sidecar(path: Path, animations: list[str], joints: dict[str, str]) -> None:
257
+ """Writes the JSON file beside a clip."""
258
+ _teams._replace(
259
+ _sidecar(path), json.dumps({'animations': animations, 'joints': joints}).encode()
260
+ )
261
+
262
+
263
+ def set_clip(
264
+ identifier: str, name: str, data: bytes, joints: dict[str, str] | None = None
265
+ ) -> dict[str, Any]:
266
+ """Stores a celebration clip of a team, or replaces the clip of that name.
267
+
268
+ Args:
269
+ identifier: The team, the built-in AI included.
270
+ name: The clip's name, up to 40 lowercase letters, digits and underscores.
271
+ data: The binary glTF 2.0 file, with a skinned skeleton and at least one animation.
272
+ joints: The bone that each joint of GRF's skeleton follows, `body` always, or an empty
273
+ mapping or `None` for the page to map the bones by their names.
274
+
275
+ Returns:
276
+ The team's new entry.
277
+
278
+ Raises:
279
+ ArgumentError: If the team is unknown, the name unsafe, the file fails a check of
280
+ `check`, the mapping names an unknown joint or bone, or the team would hold more
281
+ than 16 clips or 40 MB of them.
282
+ """
283
+ _teams.entry(identifier)
284
+ check_name(name)
285
+ found = check(data)
286
+ mapping = _check_joints(joints, found['bones'])
287
+ folder = _folder(identifier)
288
+ path = folder / f'{name}.glb'
289
+ with _teams._LOCK:
290
+ others = [other for other in _clip_files(folder) if other != path]
291
+ if len(others) >= MAXIMUM_CLIPS:
292
+ raise MessageError('server.clips.count', most=MAXIMUM_CLIPS)
293
+ total = sum(other.stat().st_size for other in others) + len(data)
294
+ if total > MAXIMUM_TEAM_BYTES:
295
+ raise MessageError('server.clips.total', total=total)
296
+ _teams._replace(path, data)
297
+ _write_sidecar(path, found['animations'], mapping)
298
+ return _teams.entry(identifier)
299
+
300
+
301
+ def set_joints(identifier: str, name: str, joints: Any) -> dict[str, Any]:
302
+ """Changes the joint mapping of a team's clip and returns the team's new entry."""
303
+ path = clip_path(identifier, name)
304
+ if path is None:
305
+ raise MessageError('server.clips.unknown', name=repr(name))
306
+ found = check(path.read_bytes())
307
+ mapping = _check_joints(joints, found['bones'])
308
+ with _teams._LOCK:
309
+ _write_sidecar(path, found['animations'], mapping)
310
+ return _teams.entry(identifier)
311
+
312
+
313
+ def clip_path(identifier: str, name: str) -> Path | None:
314
+ """Returns the file of a team's clip, or `None`."""
315
+ if identifier != _teams.BUILTIN and not _teams._home.valid_identifier(identifier):
316
+ return None
317
+ try:
318
+ check_name(name)
319
+ except ArgumentError:
320
+ return None
321
+ return _teams._exact(_folder(identifier), f'{name}.glb')
322
+
323
+
324
+ def remove_clip(identifier: str, name: str) -> bool:
325
+ """Deletes a team's clip and returns whether it existed.
326
+
327
+ The team's celebrations that named the clip's animations go with it, and a player who had
328
+ one of them as his own takes the team's celebrations again.
329
+ """
330
+ path = clip_path(identifier, name)
331
+ if path is None:
332
+ return False
333
+ with _teams._LOCK:
334
+ path.unlink()
335
+ _sidecar(path).unlink(missing_ok=True)
336
+ current = _teams.entry(identifier)
337
+ prefix = f'upload:{name}.'
338
+ style = current['style']
339
+ style['celebrations'] = [
340
+ item for item in style['celebrations'] if not item.startswith(prefix)
341
+ ]
342
+ for player in style['players']:
343
+ if str(player.get('celebration', '')).startswith(prefix):
344
+ player['celebration'] = 'team'
345
+ _teams._write(identifier, current)
346
+ return True
347
+
348
+
349
+ def import_folder(team_folder: Path) -> None:
350
+ """Checks the clips of an exported team as uploads are checked, and sets aside what fails.
351
+
352
+ The mapping of a clip comes back when it names joints and bones of the clip, and is left to
353
+ the page's guess otherwise. Clips beyond the limits of a team are set aside in name order.
354
+ A clip set aside keeps its bytes under its name followed by `.rejected`, and files that the
355
+ page never reads as clips stay as they are.
356
+ """
357
+ folder = team_folder / FOLDER
358
+ if not (folder.exists() or folder.is_symlink()) or not _teams._kept(folder):
359
+ return
360
+ if not folder.is_dir():
361
+ _teams.set_aside(folder, 'it is no folder of clips')
362
+ return
363
+ kept = 0
364
+ total = 0
365
+ for path in sorted(folder.iterdir()):
366
+ if not _teams._kept(path):
367
+ continue
368
+ if path.suffix != '.glb' or not _NAME.fullmatch(path.stem):
369
+ # A name that is a clip's name in another case, such as `foo.GLB`, is set aside, so no
370
+ # system that finds names whatever their case reads it as a clip.
371
+ lower = path.name.lower()
372
+ if lower.endswith('.glb') and _NAME.fullmatch(lower[: -len('.glb')]):
373
+ _teams.set_aside(path, 'its name is the name of a clip in another case')
374
+ continue
375
+ try:
376
+ data = path.read_bytes()
377
+ found = check(data)
378
+ valid = kept < MAXIMUM_CLIPS and total + len(data) <= MAXIMUM_TEAM_BYTES
379
+ except Exception:
380
+ # A clip that fails any part of its check, however it fails, counts as failing.
381
+ valid = False
382
+ if not valid:
383
+ _teams.set_aside(path, 'it fails the checks of a clip or the limits of a team')
384
+ continue
385
+ kept += 1
386
+ total += len(data)
387
+ joints: Any = None
388
+ with contextlib.suppress(OSError, ValueError, KeyError, TypeError, AttributeError):
389
+ joints = json.loads(_sidecar(path).read_text(encoding='utf-8')).get('joints')
390
+ try:
391
+ mapping = _check_joints(joints, found['bones'])
392
+ except ArgumentError:
393
+ mapping = {}
394
+ _write_sidecar(path, found['animations'], mapping)
@@ -0,0 +1,108 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The joints of a team's own model, the bones that stand for GRF's nodes.
5
+
6
+ A team's `model.glb` in its `cuju-team` folder draws its players through GRF's 14 nodes, so each
7
+ node it maps names a skinned bone of the model, and `body`, the pelvis, is the one it needs. The
8
+ team editor saves that mapping in `team.json` under `model.joints`. A folder that brings a model
9
+ without one gets the mapping the editor would guess from the bone names, Mixamo's first and then a
10
+ few other common rigs, as `guessJoints` of `web/js/players/players.js` does, so an import and
11
+ `cuju render pack check` read the same model the same way.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from typing import Any
18
+
19
+ from cuju.errors import ArgumentError
20
+
21
+ # GRF's nodes in their order, the player's root first.
22
+ NODES = (
23
+ 'player',
24
+ 'body',
25
+ 'middle',
26
+ 'neck',
27
+ 'left_shoulder',
28
+ 'left_elbow',
29
+ 'right_shoulder',
30
+ 'right_elbow',
31
+ 'left_thigh',
32
+ 'left_knee',
33
+ 'left_ankle',
34
+ 'right_thigh',
35
+ 'right_knee',
36
+ 'right_ankle',
37
+ )
38
+ # Common rig names of each node, as web/js/players/players.js lists them.
39
+ RIG_NAMES = {
40
+ 'player': ('root', 'Root', 'Armature'),
41
+ 'body': ('Hips', 'hips', 'pelvis', 'Pelvis'),
42
+ 'middle': ('Spine2', 'Spine1', 'spine_02', 'Chest', 'chest', 'Spine', 'spine'),
43
+ 'neck': ('Neck', 'neck', 'neck_01', 'Head', 'head'),
44
+ 'left_shoulder': ('LeftArm', 'upperarm_l', 'UpperArm.L', 'upper_arm.L', 'LeftUpperArm'),
45
+ 'left_elbow': ('LeftForeArm', 'lowerarm_l', 'LowerArm.L', 'forearm.L', 'LeftLowerArm'),
46
+ 'right_shoulder': ('RightArm', 'upperarm_r', 'UpperArm.R', 'upper_arm.R', 'RightUpperArm'),
47
+ 'right_elbow': ('RightForeArm', 'lowerarm_r', 'LowerArm.R', 'forearm.R', 'RightLowerArm'),
48
+ 'left_thigh': ('LeftUpLeg', 'thigh_l', 'UpperLeg.L', 'thigh.L', 'LeftUpperLeg'),
49
+ 'left_knee': ('LeftLeg', 'calf_l', 'LowerLeg.L', 'shin.L', 'LeftLowerLeg'),
50
+ 'left_ankle': ('LeftFoot', 'foot_l', 'Foot.L', 'foot.L'),
51
+ 'right_thigh': ('RightUpLeg', 'thigh_r', 'UpperLeg.R', 'thigh.R', 'RightUpperLeg'),
52
+ 'right_knee': ('RightLeg', 'calf_r', 'LowerLeg.R', 'shin.R', 'RightLowerLeg'),
53
+ 'right_ankle': ('RightFoot', 'foot_r', 'Foot.R', 'foot.R'),
54
+ }
55
+
56
+
57
+ def _stripped(name: str) -> str:
58
+ """Returns a bone name without the prefix of Mixamo or of a namespace."""
59
+ name = re.sub(r'^mixamorig\d*[:_]?', '', name, flags=re.IGNORECASE)
60
+ return re.sub(r'^.*[:|]', '', name)
61
+
62
+
63
+ def guess(bones: Any) -> dict[str, str]:
64
+ """Returns the mapping of GRF's nodes that the bone names give, exact names first."""
65
+ names = [bone for bone in bones if isinstance(bone, str)]
66
+ exact = set(names)
67
+ stripped: dict[str, str] = {}
68
+ for bone in names:
69
+ stripped.setdefault(_stripped(bone).lower(), bone)
70
+ joints = {}
71
+ for node in NODES:
72
+ if node in exact:
73
+ joints[node] = node
74
+ continue
75
+ for candidate in RIG_NAMES[node]:
76
+ hit = stripped.get(candidate.lower())
77
+ if hit:
78
+ joints[node] = hit
79
+ break
80
+ return joints
81
+
82
+
83
+ def resolve(joints: Any, bones: set[str]) -> tuple[dict[str, str], bool]:
84
+ """Returns the joints a team's model keeps on import and whether they were guessed.
85
+
86
+ Args:
87
+ joints: The `model.joints` of `team.json`, or `None` when it gives none.
88
+ bones: The names of the skinned bones of the model.
89
+
90
+ Raises:
91
+ ArgumentError: If the joints map no bone to `body` or name bones the model lacks.
92
+ """
93
+ guessed = not isinstance(joints, dict)
94
+ if guessed:
95
+ joints = guess(sorted(bones))
96
+ if 'body' not in joints:
97
+ raise ArgumentError(
98
+ 'expected model.joints in team.json with a bone for body, and no bone of the '
99
+ 'model has a name the team editor knows, such as Hips'
100
+ )
101
+ if 'body' not in joints:
102
+ raise ArgumentError('expected model.joints in team.json with a bone for body')
103
+ unknown = sorted(
104
+ str(bone) for bone in joints.values() if not isinstance(bone, str) or bone not in bones
105
+ )
106
+ if unknown:
107
+ raise ArgumentError(f'expected model.joints to name skinned bones, found {unknown[0]}')
108
+ return {str(key): value for key, value in joints.items() if key in NODES}, guessed