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,224 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The celebration clips that a team uploads, binary glTF files with a skinned skeleton and one or
|
|
5
|
+
// more animations. Each animation is read at 30 frames a second on a rig of the clip's own
|
|
6
|
+
// skeleton, with the joint mapping that a custom model uses, and turned into GRF's nodes by
|
|
7
|
+
// retarget.js, so it plays on GRF's player and on any model as the procedural moves do.
|
|
8
|
+
//
|
|
9
|
+
// A team's style names an animation by the prefix upload, the clip's name and the animation's
|
|
10
|
+
// index, as PREFIX below shows. Two teams may hold clips of the same name, so while a match plays
|
|
11
|
+
// the names of each team's style carry the tag of that team as well, after a space.
|
|
12
|
+
|
|
13
|
+
import * as THREE from 'three';
|
|
14
|
+
import { api } from '../core/api.js';
|
|
15
|
+
import { Rig, fileBoneNames, guessJoints, parseModel } from './players.js';
|
|
16
|
+
import {
|
|
17
|
+
FPS, MAX_SECONDS, REST_BODY_HEIGHT, forgetUploads, hasUpload, nodeClip, registerUpload,
|
|
18
|
+
} from './retarget.js';
|
|
19
|
+
import { t } from '../core/i18n.js';
|
|
20
|
+
|
|
21
|
+
export const MAX_CLIP_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
export const MAX_TEAM_CLIP_BYTES = 40 * 1024 * 1024;
|
|
23
|
+
export const MAX_CLIPS = 16;
|
|
24
|
+
const PREFIX = 'upload:';
|
|
25
|
+
// Windows opens a device for these names, the server refuses them as clip names.
|
|
26
|
+
const DEVICES = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/;
|
|
27
|
+
|
|
28
|
+
export function isUploadName(name) {
|
|
29
|
+
return typeof name === 'string' && name.startsWith(PREFIX);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The name of a clip made from the name of its file, lower case letters, digits and underscores.
|
|
33
|
+
export function clipNameOf(filename) {
|
|
34
|
+
let name = String(filename || '').replace(/\.glb$/i, '').toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 40);
|
|
36
|
+
if (!name || DEVICES.test(name)) {
|
|
37
|
+
name = `clip_${name}`.replace(/_+$/, '').slice(0, 40) || 'clip';
|
|
38
|
+
}
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The celebrations a team's clips offer, each its name in a style and the animation's name.
|
|
43
|
+
export function uploadMoves(team) {
|
|
44
|
+
const clips = team && Array.isArray(team.clips) ? team.clips : [];
|
|
45
|
+
return clips.flatMap((c) => (c.animations || []).map((label, i) => [`${PREFIX}${c.name}.${i}`, label, c.name]));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The animation's name of a celebration that names a team's clip, or null.
|
|
49
|
+
export function uploadMoveLabel(team, value) {
|
|
50
|
+
const found = uploadMoves(team).find(([v]) => v === value);
|
|
51
|
+
return found ? found[1] : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Checks the bytes of a clip as the server will, and reads it. Returns the parsed file, the names
|
|
55
|
+
// of its skinned bones and the mapping guessed from them, or throws an error to show.
|
|
56
|
+
export async function readClip(buffer) {
|
|
57
|
+
if (buffer.byteLength > MAX_CLIP_BYTES) {
|
|
58
|
+
throw new Error(t('clips.tooLarge', { size: Math.ceil(buffer.byteLength / 1048576) }));
|
|
59
|
+
}
|
|
60
|
+
const gltf = await parseModel(buffer);
|
|
61
|
+
const bones = fileBoneNames(gltf);
|
|
62
|
+
if (!gltf.animations || !gltf.animations.length) {
|
|
63
|
+
throw new Error(t('clips.noAnimation'));
|
|
64
|
+
}
|
|
65
|
+
return { gltf, bones, guess: guessJoints(bones) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Reads one animation of a parsed clip on the clip's own skeleton and returns a clip of nodes. The
|
|
69
|
+
// rig of the skeleton may be given, so the animations of one file share it.
|
|
70
|
+
export function retargetAnimation(gltf, animation, joints, rig = new Rig(gltf.scene, joints, {})) {
|
|
71
|
+
const corrections = new Array(14).fill(null);
|
|
72
|
+
const index = new Array(14).fill(-1);
|
|
73
|
+
rig.bones.forEach((b, j) => {
|
|
74
|
+
if (b.node > 0 && b.correction) {
|
|
75
|
+
corrections[b.node] = b.correction.toArray();
|
|
76
|
+
index[b.node] = j;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// The rig places the pelvis this high above the clip's floor at rest, in its own frame.
|
|
80
|
+
const floor = rig.bones[index[1]].position.y - rig.heightRatio * REST_BODY_HEIGHT;
|
|
81
|
+
const instance = rig.instantiate();
|
|
82
|
+
for (const b of instance.bones) {
|
|
83
|
+
if (b) {
|
|
84
|
+
b.matrixAutoUpdate = true;
|
|
85
|
+
b.matrixWorldAutoUpdate = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const mixer = new THREE.AnimationMixer(instance.root);
|
|
89
|
+
const action = mixer.clipAction(animation);
|
|
90
|
+
action.play();
|
|
91
|
+
const duration = Math.min(MAX_SECONDS, Math.max(0, animation.duration || 0));
|
|
92
|
+
const count = duration > 0 ? Math.max(2, Math.round(duration * FPS) + 1) : 1;
|
|
93
|
+
const matrix = new THREE.Matrix4();
|
|
94
|
+
const p = new THREE.Vector3();
|
|
95
|
+
const q = new THREE.Quaternion();
|
|
96
|
+
const s = new THREE.Vector3();
|
|
97
|
+
const samples = [];
|
|
98
|
+
try {
|
|
99
|
+
for (let i = 0; i < count; i++) {
|
|
100
|
+
const time = count > 1 ? (i / (count - 1)) * duration : 0;
|
|
101
|
+
action.time = Math.max(0, Math.min(time, (animation.duration || 0) - 1e-4));
|
|
102
|
+
mixer.update(0);
|
|
103
|
+
instance.root.updateMatrixWorld(true);
|
|
104
|
+
const bones = new Array(14).fill(null);
|
|
105
|
+
let body = [0, 0, 0];
|
|
106
|
+
for (let n = 1; n < 14; n++) {
|
|
107
|
+
const j = index[n];
|
|
108
|
+
const bone = j >= 0 ? instance.bones[j] : null;
|
|
109
|
+
if (!bone) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
matrix.multiplyMatrices(rig.placement, bone.matrixWorld).decompose(p, q, s);
|
|
113
|
+
bones[n] = q.toArray();
|
|
114
|
+
if (n === 1) {
|
|
115
|
+
body = p.toArray();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
samples.push({ bones, body });
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
mixer.stopAllAction();
|
|
122
|
+
mixer.uncacheRoot(instance.root);
|
|
123
|
+
rig.release(instance);
|
|
124
|
+
}
|
|
125
|
+
return nodeClip(samples, { corrections, heightRatio: rig.heightRatio, floor }, duration);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The tag of each team whose clips the page loaded, so the names of two teams never meet.
|
|
129
|
+
const tags = new Map();
|
|
130
|
+
const loads = new Map();
|
|
131
|
+
|
|
132
|
+
function tagOf(id) {
|
|
133
|
+
if (!tags.has(id)) {
|
|
134
|
+
tags.set(id, `c${tags.size + 1}`);
|
|
135
|
+
}
|
|
136
|
+
return tags.get(id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The name a celebration of a team plays under while its clips are loaded, the name itself for
|
|
140
|
+
// every other move.
|
|
141
|
+
export function qualify(value, tag) {
|
|
142
|
+
if (!tag || !isUploadName(value)) {
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
const name = `${value} ${tag}`;
|
|
146
|
+
return hasUpload(name) ? name : value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The name in a style of a celebration that plays under a tagged name.
|
|
150
|
+
export function unqualify(name) {
|
|
151
|
+
return isUploadName(name) ? name.replace(/ c\d+$/, '') : name;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Gives the celebrations of a style the names their loaded clips play under, in place.
|
|
155
|
+
export function qualifyStyle(style, tag) {
|
|
156
|
+
if (!style || !tag) {
|
|
157
|
+
return style;
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(style.celebrations)) {
|
|
160
|
+
style.celebrations = style.celebrations.map((v) => qualify(v, tag));
|
|
161
|
+
}
|
|
162
|
+
for (const p of Array.isArray(style.players) ? style.players : []) {
|
|
163
|
+
if (p && typeof p.celebration === 'string') {
|
|
164
|
+
p.celebration = qualify(p.celebration, tag);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return style;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Loads the clips of a registry team and returns the team's tag, or null when it holds none.
|
|
171
|
+
// The clips load once while they stay the same, and a clip that fails is left out quietly, so its
|
|
172
|
+
// celebrations give way to the procedural moves.
|
|
173
|
+
export async function loadTeamClips(team) {
|
|
174
|
+
if (!team || !team.id || !Array.isArray(team.clips) || !team.clips.length) {
|
|
175
|
+
if (team && team.id && tags.has(team.id)) {
|
|
176
|
+
forgetUploads(` ${tags.get(team.id)}`);
|
|
177
|
+
loads.delete(team.id);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const tag = tagOf(team.id);
|
|
182
|
+
const signature = JSON.stringify(team.clips);
|
|
183
|
+
const cached = loads.get(team.id);
|
|
184
|
+
if (cached && cached.signature === signature) {
|
|
185
|
+
await cached.promise;
|
|
186
|
+
return tag;
|
|
187
|
+
}
|
|
188
|
+
const entry = { signature, promise: null };
|
|
189
|
+
loads.set(team.id, entry);
|
|
190
|
+
entry.promise = (async () => {
|
|
191
|
+
const made = [];
|
|
192
|
+
for (const clip of team.clips) {
|
|
193
|
+
try {
|
|
194
|
+
const buffer = await api.clipBytes(team.id, clip.name);
|
|
195
|
+
const { gltf, guess } = await readClip(buffer);
|
|
196
|
+
const joints = clip.joints && clip.joints.body ? clip.joints : guess;
|
|
197
|
+
if (!joints.body) {
|
|
198
|
+
console.warn('clip without a pelvis left out', clip.name);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const rig = new Rig(gltf.scene, joints, {});
|
|
202
|
+
gltf.animations.forEach((animation, i) => {
|
|
203
|
+
try {
|
|
204
|
+
const label = (clip.animations && clip.animations[i]) || animation.name || t('clips.animation', { n: i + 1 });
|
|
205
|
+
made.push([`${PREFIX}${clip.name}.${i} ${tag}`, retargetAnimation(gltf, animation, joints, rig), label]);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.warn('clip animation left out', clip.name, i, err);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.warn('clip left out', clip.name, err);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Only the newest load of a team registers its clips.
|
|
215
|
+
if (loads.get(team.id) === entry) {
|
|
216
|
+
forgetUploads(` ${tag}`);
|
|
217
|
+
for (const [name, nodes, label] of made) {
|
|
218
|
+
registerUpload(name, nodes, label);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
222
|
+
await entry.promise;
|
|
223
|
+
return tag;
|
|
224
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The animation clips of Kay Lousberg's KayKit Character Animations, made for Rig_Medium, the
|
|
5
|
+
// rig that every shipped KayKit character shares. The files load once, the first time a model on
|
|
6
|
+
// that rig is used, and their clips join every such rig, so a player plays them by name.
|
|
7
|
+
|
|
8
|
+
import * as THREE from 'three';
|
|
9
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
10
|
+
import { t } from '../core/i18n.js';
|
|
11
|
+
|
|
12
|
+
const PACKS = ['general', 'movementbasic', 'movementadvanced', 'simulation', 'special', 'combatmelee'];
|
|
13
|
+
const URL_OF = (pack) => `assets/animations/kaykit-rig-medium-${pack}.glb`;
|
|
14
|
+
|
|
15
|
+
// Clips with nothing to show on a football pitch, or that only reset the rig.
|
|
16
|
+
const SKIPPED = /^(T-Pose|EXPERIMENTAL_.*)$/;
|
|
17
|
+
|
|
18
|
+
// Bones every model on Rig_Medium carries, how a rig of the family is told apart.
|
|
19
|
+
const FAMILY_BONES = ['root', 'hips', 'spine', 'chest', 'head', 'upperarml', 'upperarmr', 'upperlegl', 'upperlegr'];
|
|
20
|
+
|
|
21
|
+
let library = null;
|
|
22
|
+
|
|
23
|
+
// Every clip of the packs that loaded, a pack that fails is left out quietly.
|
|
24
|
+
export function kaykitClips() {
|
|
25
|
+
if (!library) {
|
|
26
|
+
library = Promise.all(PACKS.map(async (pack) => {
|
|
27
|
+
try {
|
|
28
|
+
const gltf = await new GLTFLoader().loadAsync(URL_OF(pack));
|
|
29
|
+
return (gltf.animations || []).filter((c) => c && c.name && !SKIPPED.test(c.name));
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
})).then((lists) => {
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const clip of lists.flat()) {
|
|
37
|
+
if (!seen.has(clip.name)) {
|
|
38
|
+
seen.add(clip.name);
|
|
39
|
+
out.push(clip);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return library;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The names of the objects of a scene, as GLTFLoader leaves them.
|
|
49
|
+
function namesOf(scene) {
|
|
50
|
+
const names = new Set();
|
|
51
|
+
scene.traverse((obj) => {
|
|
52
|
+
if (obj.name) {
|
|
53
|
+
names.add(obj.name);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return names;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Whether a scene is a model on Rig_Medium.
|
|
60
|
+
export function isKayKitScene(scene) {
|
|
61
|
+
if (!scene) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const names = namesOf(scene);
|
|
65
|
+
return names.has('Rig_Medium') || FAMILY_BONES.every((n) => names.has(n));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Adds the KayKit clips to a rig on Rig_Medium, each cut down to the tracks of bones the model
|
|
69
|
+
// has, so a model without hand slots binds every track it keeps. A clip the model's own file
|
|
70
|
+
// carries under the same name stays. Resolves to the rig, with or without the clips.
|
|
71
|
+
export async function attachKayKitClips(rig) {
|
|
72
|
+
if (!rig || !rig.scene || !Array.isArray(rig.clips) || rig.kaykit || !isKayKitScene(rig.scene)) {
|
|
73
|
+
return rig;
|
|
74
|
+
}
|
|
75
|
+
rig.kaykit = true;
|
|
76
|
+
try {
|
|
77
|
+
const clips = await kaykitClips();
|
|
78
|
+
const names = namesOf(rig.scene);
|
|
79
|
+
const own = new Set(rig.clips.map((c) => c.name));
|
|
80
|
+
for (const clip of clips) {
|
|
81
|
+
if (own.has(clip.name)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const tracks = clip.tracks.filter((track) => {
|
|
85
|
+
const node = THREE.PropertyBinding.parseTrackName(track.name).nodeName;
|
|
86
|
+
return names.has(node);
|
|
87
|
+
});
|
|
88
|
+
if (tracks.length) {
|
|
89
|
+
rig.clips.push(new THREE.AnimationClip(clip.name, clip.duration, tracks));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// The procedural moves stand in for any clip that did not load.
|
|
94
|
+
}
|
|
95
|
+
return rig;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clips for each role on a KayKit model, the first one the model has wins. A name with a greater
|
|
99
|
+
// than sign plays its first clip once and then loops the second.
|
|
100
|
+
export const KAYKIT_ROLES = {
|
|
101
|
+
celebrate: ['clip:Cheering', 'clip:Jump_Full_Long', 'clip:Skeletons_Taunt_Longer', 'clip:Jump_Full_Short', 'clip:Waving', 'clip:Push_Ups'],
|
|
102
|
+
jump: ['clip:Jump_Full_Short', 'clip:Jump_Full_Long', 'clip:Cheering'],
|
|
103
|
+
cheer: ['clip:Cheering', 'clip:Jump_Full_Short'],
|
|
104
|
+
taunt: ['clip:Skeletons_Taunt_Longer', 'clip:Skeletons_Taunt', 'clip:Cheering'],
|
|
105
|
+
showoff: ['clip:Push_Ups', 'clip:Skeletons_Taunt'],
|
|
106
|
+
wave: ['clip:Waving', 'clip:Cheering'],
|
|
107
|
+
huddle: ['clip:Cheering', 'clip:Idle_B'],
|
|
108
|
+
idle: ['clip:Idle_A', 'clip:Idle_B'],
|
|
109
|
+
calm: ['clip:Idle_B', 'clip:Idle_A'],
|
|
110
|
+
dejected: ['clip:Sit_Floor_Down>Sit_Floor_Idle', 'clip:Crouching', 'clip:Idle_B'],
|
|
111
|
+
crouch: ['clip:Crouching', 'clip:Idle_B'],
|
|
112
|
+
floored: ['clip:Lie_Down>Lie_Idle', 'clip:Sit_Floor_Down>Sit_Floor_Idle'],
|
|
113
|
+
handshake: ['clip:Interact', 'clip:Idle_A'],
|
|
114
|
+
punch: ['clip:Melee_Unarmed_Attack_Punch_A', 'clip:Cheering'],
|
|
115
|
+
kick: ['clip:Melee_Unarmed_Attack_Kick', 'clip:Cheering'],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// The celebrations a KayKit scorer picks from when his team leaves the choice open, with the
|
|
119
|
+
// air punch and the kick of the combat pack for a little more swagger.
|
|
120
|
+
export const KAYKIT_CELEBRATIONS = [
|
|
121
|
+
'clip:Cheering', 'clip:Jump_Full_Long', 'clip:Skeletons_Taunt_Longer', 'clip:Push_Ups', 'clip:Waving',
|
|
122
|
+
'clip:Jump_Full_Short', 'clip:Melee_Unarmed_Attack_Punch_A', 'clip:Skeletons_Taunt',
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// The KayKit clips the team editor offers as celebrations, with their names in the editor.
|
|
126
|
+
export const KAYKIT_MENU = [
|
|
127
|
+
['Cheering', t('kaykit.cheering')],
|
|
128
|
+
['Waving', t('kaykit.waving')],
|
|
129
|
+
['Jump_Full_Short', t('kaykit.jump')],
|
|
130
|
+
['Jump_Full_Long', t('kaykit.big_jump')],
|
|
131
|
+
['Skeletons_Taunt', t('kaykit.taunt')],
|
|
132
|
+
['Skeletons_Taunt_Longer', t('kaykit.long_taunt')],
|
|
133
|
+
['Push_Ups', t('kaykit.push_ups')],
|
|
134
|
+
['Sit_Ups', t('kaykit.sit_ups')],
|
|
135
|
+
['Melee_Unarmed_Attack_Punch_A', t('kaykit.air_punch')],
|
|
136
|
+
['Melee_Unarmed_Attack_Kick', t('kaykit.air_kick')],
|
|
137
|
+
['Dodge_Forward', t('kaykit.forward_roll')],
|
|
138
|
+
['Sit_Floor_Down>Sit_Floor_Idle', t('kaykit.sit')],
|
|
139
|
+
['Lie_Down>Lie_Idle', t('kaykit.lie')],
|
|
140
|
+
['Idle_B', t('kaykit.stand_cool')],
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const LABELS = new Map(KAYKIT_MENU);
|
|
144
|
+
|
|
145
|
+
// The editor's name of a KayKit clip, or null for another name.
|
|
146
|
+
export function kaykitLabel(name) {
|
|
147
|
+
return LABELS.get(String(name || '').replace(/^clip:/, '')) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The clips the editor lists for a rig, by name without the clip prefix. A KayKit rig lists the
|
|
151
|
+
// menu above, then the clips of its own file, any other rig every clip it has.
|
|
152
|
+
export function menuClips(rig) {
|
|
153
|
+
if (!rig || !Array.isArray(rig.clips)) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (!rig.kaykit) {
|
|
157
|
+
return rig.clips.map((c) => c.name);
|
|
158
|
+
}
|
|
159
|
+
const has = (name) => name.split('>').every((part) => rig.clips.some((c) => c.name === part));
|
|
160
|
+
const own = ((rig.options && rig.options.animations) || []).map((c) => c && c.name).filter(Boolean);
|
|
161
|
+
return [...KAYKIT_MENU.map(([name]) => name).filter(has), ...own.filter((n) => !LABELS.has(n))];
|
|
162
|
+
}
|