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.
- cuju/render/__init__.py +759 -0
- cuju/render/__main__.py +1065 -0
- cuju/render/_export/__init__.py +3 -0
- cuju/render/_export/feed.py +542 -0
- cuju/render/_export/sizes.py +157 -0
- cuju/render/_export/video.py +1618 -0
- cuju/render/_export/video_jobs.py +408 -0
- cuju/render/_format/__init__.py +3 -0
- cuju/render/_format/annotations.py +235 -0
- cuju/render/_format/codec.py +307 -0
- cuju/render/_format/keep.py +121 -0
- cuju/render/_format/recording.py +860 -0
- cuju/render/_format/writer.py +54 -0
- cuju/render/_guide/__init__.py +3 -0
- cuju/render/_guide/doctor.py +523 -0
- cuju/render/_guide/help.py +62 -0
- cuju/render/_guide/tutorial.py +254 -0
- cuju/render/_http/__init__.py +3 -0
- cuju/render/_http/commands.py +57 -0
- cuju/render/_http/headers.py +103 -0
- cuju/render/_http/launch.py +500 -0
- cuju/render/_http/launchers.py +408 -0
- cuju/render/_http/pwa.py +128 -0
- cuju/render/_http/server.py +2105 -0
- cuju/render/_http/window.py +360 -0
- cuju/render/_moments/__init__.py +3 -0
- cuju/render/_moments/captions.py +284 -0
- cuju/render/_moments/events.py +1123 -0
- cuju/render/_moments/search.py +694 -0
- cuju/render/_packages/__init__.py +3 -0
- cuju/render/_packages/glsl.py +746 -0
- cuju/render/_packages/loudness.py +334 -0
- cuju/render/_packages/pack.py +1959 -0
- cuju/render/_packages/presentation.py +1376 -0
- cuju/render/_packages/vocabulary.py +609 -0
- cuju/render/_playing/__init__.py +3 -0
- cuju/render/_playing/achievements.py +463 -0
- cuju/render/_playing/bench.py +609 -0
- cuju/render/_playing/challenges.py +1813 -0
- cuju/render/_playing/demo.py +323 -0
- cuju/render/_playing/draw.py +211 -0
- cuju/render/_playing/errors.py +20 -0
- cuju/render/_playing/game.py +645 -0
- cuju/render/_playing/limits.py +339 -0
- cuju/render/_playing/play.py +926 -0
- cuju/render/_playing/policy_worker.py +250 -0
- cuju/render/_playing/strict.py +810 -0
- cuju/render/_registry/__init__.py +3 -0
- cuju/render/_registry/clips.py +394 -0
- cuju/render/_registry/joints.py +108 -0
- cuju/render/_registry/teams.py +2058 -0
- cuju/render/_store/__init__.py +3 -0
- cuju/render/_store/home.py +70 -0
- cuju/render/_store/library.py +604 -0
- cuju/render/_store/samples.py +163 -0
- cuju/render/_text/__init__.py +3 -0
- cuju/render/_text/languages.py +637 -0
- cuju/render/_text/messages.py +1396 -0
- cuju/render/_tournaments/__init__.py +3 -0
- cuju/render/_tournaments/results.py +547 -0
- cuju/render/_tournaments/tournament.py +2742 -0
- cuju/render/_tournaments/tournament_cli.py +587 -0
- cuju/render/_tournaments/tournament_runner.py +1975 -0
- cuju/render/_version.py +6 -0
- cuju/render/_watch/__init__.py +3 -0
- cuju/render/_watch/grid.py +265 -0
- cuju/render/_watch/live.py +664 -0
- cuju/render/_watch/notebook.py +79 -0
- cuju/render/_watch/show.py +171 -0
- cuju/render/samples/sample-11v11.cjr +0 -0
- cuju/render/samples/sample-5v5.cjr +0 -0
- cuju/render/samples/teams/starter/main.py +33 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
- cuju/render/web/assets/adboard_polygon.webp +0 -0
- cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
- cuju/render/web/assets/ball.glb +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/corner.mp4 +0 -0
- cuju/render/web/assets/challenges/corner.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
- cuju/render/web/assets/challenges/manifest.json +375 -0
- cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one.webp +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
- cuju/render/web/assets/crowd.webp +0 -0
- cuju/render/web/assets/crowd_normal.webp +0 -0
- cuju/render/web/assets/cuju-logo-384.png +0 -0
- cuju/render/web/assets/cuju-logo.png +0 -0
- cuju/render/web/assets/cuju-logo.svg +46 -0
- cuju/render/web/assets/cuju-mark-light.svg +33 -0
- cuju/render/web/assets/cuju-mark.svg +33 -0
- cuju/render/web/assets/goal.glb +0 -0
- cuju/render/web/assets/grass.webp +0 -0
- cuju/render/web/assets/hair_black.png +0 -0
- cuju/render/web/assets/hair_blonde.png +0 -0
- cuju/render/web/assets/hair_brown.png +0 -0
- cuju/render/web/assets/hair_darkblonde.png +0 -0
- cuju/render/web/assets/hair_long01.glb +0 -0
- cuju/render/web/assets/hair_long02.glb +0 -0
- cuju/render/web/assets/hair_medium01.glb +0 -0
- cuju/render/web/assets/hair_red.png +0 -0
- cuju/render/web/assets/hair_short01.glb +0 -0
- cuju/render/web/assets/hair_short02.glb +0 -0
- cuju/render/web/assets/icons/cuju-192.png +0 -0
- cuju/render/web/assets/icons/cuju-512.png +0 -0
- cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
- cuju/render/web/assets/kit_goalkeeper.webp +0 -0
- cuju/render/web/assets/kit_mask.png +0 -0
- cuju/render/web/assets/kit_normal.webp +0 -0
- cuju/render/web/assets/manifest.json +601 -0
- cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
- cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
- cuju/render/web/assets/models/kaykit-knight.glb +0 -0
- cuju/render/web/assets/models/kaykit-knight.json +23 -0
- cuju/render/web/assets/models/kaykit-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
- cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
- cuju/render/web/assets/models/kaykit-ranger.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
- cuju/render/web/assets/pitch.webp +0 -0
- cuju/render/web/assets/pitch_normal.webp +0 -0
- cuju/render/web/assets/player.glb +0 -0
- cuju/render/web/assets/shoe.webp +0 -0
- cuju/render/web/assets/shoe_sole.png +0 -0
- cuju/render/web/assets/skin_01.png +0 -0
- cuju/render/web/assets/skin_02.png +0 -0
- cuju/render/web/assets/skin_03.png +0 -0
- cuju/render/web/assets/skin_04.png +0 -0
- cuju/render/web/assets/sounds/applause.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
- cuju/render/web/assets/sounds/city-horn.ogg +0 -0
- cuju/render/web/assets/sounds/city-loop.ogg +0 -0
- cuju/render/web/assets/sounds/city-siren.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
- cuju/render/web/assets/sounds/gulls.ogg +0 -0
- cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
- cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
- cuju/render/web/assets/sounds/sounds.json +46 -0
- cuju/render/web/assets/sounds/space-beep.ogg +0 -0
- cuju/render/web/assets/sounds/space-hum.ogg +0 -0
- cuju/render/web/assets/sounds/space-radio.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
- cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
- cuju/render/web/easy.css +357 -0
- cuju/render/web/export.html +23 -0
- cuju/render/web/feed.css +262 -0
- cuju/render/web/feed.html +28 -0
- cuju/render/web/flat.css +120 -0
- cuju/render/web/home.css +837 -0
- cuju/render/web/index.html +373 -0
- cuju/render/web/js/boot.js +50 -0
- cuju/render/web/js/broadcast/audio.js +2141 -0
- cuju/render/web/js/broadcast/celebrate.js +1238 -0
- cuju/render/web/js/broadcast/decisions.js +609 -0
- cuju/render/web/js/broadcast/hud.js +352 -0
- cuju/render/web/js/broadcast/show.js +3614 -0
- cuju/render/web/js/broadcast/transition.js +2231 -0
- cuju/render/web/js/broadcast/voices.js +153 -0
- cuju/render/web/js/capture.js +101 -0
- cuju/render/web/js/core/accessibility.js +90 -0
- cuju/render/web/js/core/api.js +429 -0
- cuju/render/web/js/core/comfort.js +135 -0
- cuju/render/web/js/core/i18n.js +705 -0
- cuju/render/web/js/core/icons.js +167 -0
- cuju/render/web/js/core/input-settings.js +156 -0
- cuju/render/web/js/core/keys.js +277 -0
- cuju/render/web/js/core/language-ui.js +125 -0
- cuju/render/web/js/core/menu.js +853 -0
- cuju/render/web/js/core/names.js +113 -0
- cuju/render/web/js/core/page-structure.js +110 -0
- cuju/render/web/js/core/scenarios.js +109 -0
- cuju/render/web/js/core/spoilers.js +236 -0
- cuju/render/web/js/core/typeahead.js +38 -0
- cuju/render/web/js/export/dom-picture.js +330 -0
- cuju/render/web/js/export/export-host.js +97 -0
- cuju/render/web/js/export/feed-dialog.js +191 -0
- cuju/render/web/js/export/feed-params.js +266 -0
- cuju/render/web/js/export/feed.js +363 -0
- cuju/render/web/js/export/mp4-mux.js +295 -0
- cuju/render/web/js/export/photo-core.js +313 -0
- cuju/render/web/js/export/photo.js +1314 -0
- cuju/render/web/js/export/safe-frame.js +59 -0
- cuju/render/web/js/export/size-fields.js +103 -0
- cuju/render/web/js/export/sizes.js +178 -0
- cuju/render/web/js/export/video-boot.js +55 -0
- cuju/render/web/js/export/video-browser.js +325 -0
- cuju/render/web/js/export/video-export.js +417 -0
- cuju/render/web/js/export/video-plan.js +207 -0
- cuju/render/web/js/export/video-range.js +185 -0
- cuju/render/web/js/export/webm-mux.js +198 -0
- cuju/render/web/js/feed-boot.js +151 -0
- cuju/render/web/js/feed-frame.js +101 -0
- cuju/render/web/js/help/doctor.js +488 -0
- cuju/render/web/js/help/firstrun.js +379 -0
- cuju/render/web/js/help/help-center.js +438 -0
- cuju/render/web/js/help/install-offer.js +164 -0
- cuju/render/web/js/help/install-state.js +104 -0
- cuju/render/web/js/help/lessons-core.js +207 -0
- cuju/render/web/js/help/lessons.js +463 -0
- cuju/render/web/js/help/palette.js +584 -0
- cuju/render/web/js/help/pwa.js +257 -0
- cuju/render/web/js/help/tour-core.js +241 -0
- cuju/render/web/js/help/tour.js +461 -0
- cuju/render/web/js/main.js +2513 -0
- cuju/render/web/js/pages/draw.js +188 -0
- cuju/render/web/js/pages/fieldpage.js +530 -0
- cuju/render/web/js/pages/home.js +521 -0
- cuju/render/web/js/pages/matches.js +779 -0
- cuju/render/web/js/pages/moments-ui.js +359 -0
- cuju/render/web/js/pages/queue-filters.js +63 -0
- cuju/render/web/js/pages/recording-groups.js +80 -0
- cuju/render/web/js/pages/recordings.js +1069 -0
- cuju/render/web/js/play/bench.js +347 -0
- cuju/render/web/js/play/challenges.js +1302 -0
- cuju/render/web/js/play/medals.js +127 -0
- cuju/render/web/js/play/play.js +1953 -0
- cuju/render/web/js/play/teasers.js +196 -0
- cuju/render/web/js/play/touch.js +350 -0
- cuju/render/web/js/play/tournament-layout.js +487 -0
- cuju/render/web/js/play/tournament-results.js +441 -0
- cuju/render/web/js/play/tournament-strict.js +560 -0
- cuju/render/web/js/play/tournament.js +2585 -0
- cuju/render/web/js/players/animate.js +972 -0
- cuju/render/web/js/players/clips.js +224 -0
- cuju/render/web/js/players/kaykit.js +162 -0
- cuju/render/web/js/players/kits.js +676 -0
- cuju/render/web/js/players/number-patch.js +82 -0
- cuju/render/web/js/players/players.js +2694 -0
- cuju/render/web/js/players/recolor.js +893 -0
- cuju/render/web/js/players/retarget.js +325 -0
- cuju/render/web/js/players/rig-numbers.js +174 -0
- cuju/render/web/js/presentation/animator.js +231 -0
- cuju/render/web/js/presentation/assign.js +85 -0
- cuju/render/web/js/presentation/behaviors.js +440 -0
- cuju/render/web/js/presentation/budget.js +209 -0
- cuju/render/web/js/presentation/catalog.js +1674 -0
- cuju/render/web/js/presentation/clocks.js +72 -0
- cuju/render/web/js/presentation/commands.js +301 -0
- cuju/render/web/js/presentation/contract.js +240 -0
- cuju/render/web/js/presentation/effects.js +904 -0
- cuju/render/web/js/presentation/flash.js +605 -0
- cuju/render/web/js/presentation/freeze-worklet.js +22 -0
- cuju/render/web/js/presentation/glsl.js +464 -0
- cuju/render/web/js/presentation/host.js +323 -0
- cuju/render/web/js/presentation/index.js +2066 -0
- cuju/render/web/js/presentation/loudness-worklet.js +235 -0
- cuju/render/web/js/presentation/loudness.js +740 -0
- cuju/render/web/js/presentation/moments.js +536 -0
- cuju/render/web/js/presentation/post.js +327 -0
- cuju/render/web/js/presentation/preview.js +180 -0
- cuju/render/web/js/presentation/protocol.js +174 -0
- cuju/render/web/js/presentation/reach.js +158 -0
- cuju/render/web/js/presentation/rigging.js +130 -0
- cuju/render/web/js/presentation/sandbox.js +157 -0
- cuju/render/web/js/presentation/shaders.js +235 -0
- cuju/render/web/js/presentation/stage.js +402 -0
- cuju/render/web/js/presentation/teamaudio.js +722 -0
- cuju/render/web/js/presentation/tracks.js +197 -0
- cuju/render/web/js/presentation/values.js +81 -0
- cuju/render/web/js/recording/annotation-data.js +203 -0
- cuju/render/web/js/recording/captions.js +491 -0
- cuju/render/web/js/recording/codec.js +185 -0
- cuju/render/web/js/recording/decode-worker.js +70 -0
- cuju/render/web/js/recording/decode.js +94 -0
- cuju/render/web/js/recording/episodes.js +46 -0
- cuju/render/web/js/recording/format.js +461 -0
- cuju/render/web/js/recording/live.js +348 -0
- cuju/render/web/js/recording/moments-query.js +59 -0
- cuju/render/web/js/recording/moments.js +149 -0
- cuju/render/web/js/recording/pose.js +86 -0
- cuju/render/web/js/recording/timeline.js +275 -0
- cuju/render/web/js/teams/art.js +50 -0
- cuju/render/web/js/teams/clip-editor.js +293 -0
- cuju/render/web/js/teams/editor.js +1979 -0
- cuju/render/web/js/teams/library.js +344 -0
- cuju/render/web/js/teams/painter.js +633 -0
- cuju/render/web/js/teams/surprise-ui.js +195 -0
- cuju/render/web/js/teams/surprise.js +497 -0
- cuju/render/web/js/teams/team-check.js +127 -0
- cuju/render/web/js/watch/annotations.js +270 -0
- cuju/render/web/js/watch/flat.js +366 -0
- cuju/render/web/js/watch/grid.js +436 -0
- cuju/render/web/js/watch/pitch2d.js +289 -0
- cuju/render/web/js/watch/player-tools.js +192 -0
- cuju/render/web/js/watch/presets.js +201 -0
- cuju/render/web/js/world/arenas.js +997 -0
- cuju/render/web/js/world/ball.js +204 -0
- cuju/render/web/js/world/cameras.js +982 -0
- cuju/render/web/js/world/crowd.js +376 -0
- cuju/render/web/js/world/field.js +227 -0
- cuju/render/web/js/world/framing.js +145 -0
- cuju/render/web/js/world/occlusion.js +48 -0
- cuju/render/web/js/world/pitch.js +703 -0
- cuju/render/web/js/world/scene.js +395 -0
- cuju/render/web/js/world/sky.js +325 -0
- cuju/render/web/js/world/stadium.js +1262 -0
- cuju/render/web/js/world/vendor-sky.js +367 -0
- cuju/render/web/js/world/weather.js +264 -0
- cuju/render/web/languages/en.json +4750 -0
- cuju/render/web/lessons.json +137 -0
- cuju/render/web/manifest.webmanifest +23 -0
- cuju/render/web/style.css +8399 -0
- cuju/render/web/sw.js +235 -0
- cuju/render/web/vendor/fabric/LICENSE +16 -0
- cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
- cuju/render/web/vendor/lucide/LICENSE +43 -0
- cuju/render/web/vendor/lucide/icons.js +141 -0
- cuju/render/web/vendor/three/LICENSE +21 -0
- cuju/render/web/vendor/three/build/three.core.js +60586 -0
- cuju/render/web/vendor/three/build/three.module.js +19719 -0
- cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
- cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
- cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
- cuju_render-0.1.0.dist-info/METADATA +116 -0
- cuju_render-0.1.0.dist-info/RECORD +382 -0
- cuju_render-0.1.0.dist-info/WHEEL +4 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
- cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
|
@@ -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
|