@volter/blender-engine 0.1.148 → 0.1.150

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 (245) hide show
  1. package/LICENSE +22 -5
  2. package/README.md +4 -11
  3. package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
  4. package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
  5. package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
  6. package/browser/addons/vrm/README.txt +3 -0
  7. package/browser/addons/vrm/__init__.py +41 -0
  8. package/browser/addons/vrm/blender_manifest.toml +16 -0
  9. package/browser/addons/vrm/common/__init__.py +1 -0
  10. package/browser/addons/vrm/common/animation.py +58 -0
  11. package/browser/addons/vrm/common/blender_manifest.py +82 -0
  12. package/browser/addons/vrm/common/char.py +16 -0
  13. package/browser/addons/vrm/common/convert.py +287 -0
  14. package/browser/addons/vrm/common/convert_any.py +48 -0
  15. package/browser/addons/vrm/common/debug.py +40 -0
  16. package/browser/addons/vrm/common/deep.py +162 -0
  17. package/browser/addons/vrm/common/error_dialog.py +274 -0
  18. package/browser/addons/vrm/common/fs.py +71 -0
  19. package/browser/addons/vrm/common/gl.py +20 -0
  20. package/browser/addons/vrm/common/gltf.py +1149 -0
  21. package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
  22. package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
  23. package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
  24. package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
  25. package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
  26. package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
  27. package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
  28. package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
  29. package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
  30. package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
  31. package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
  32. package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
  33. package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
  34. package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
  35. package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
  36. package/browser/addons/vrm/common/legacy_gltf.py +16 -0
  37. package/browser/addons/vrm/common/logger.py +84 -0
  38. package/browser/addons/vrm/common/mtoon0.blend +0 -0
  39. package/browser/addons/vrm/common/mtoon1.blend +0 -0
  40. package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
  41. package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
  42. package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
  43. package/browser/addons/vrm/common/ops/__init__.py +10 -0
  44. package/browser/addons/vrm/common/ops/export_scene.py +67 -0
  45. package/browser/addons/vrm/common/ops/icyp.py +53 -0
  46. package/browser/addons/vrm/common/ops/import_scene.py +57 -0
  47. package/browser/addons/vrm/common/ops/vrm.py +2380 -0
  48. package/browser/addons/vrm/common/ops/wm.py +167 -0
  49. package/browser/addons/vrm/common/preferences.py +362 -0
  50. package/browser/addons/vrm/common/progress.py +87 -0
  51. package/browser/addons/vrm/common/rotation.py +89 -0
  52. package/browser/addons/vrm/common/safe_removal.py +46 -0
  53. package/browser/addons/vrm/common/scene_watcher.py +230 -0
  54. package/browser/addons/vrm/common/shader.py +2243 -0
  55. package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
  56. package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
  57. package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
  58. package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
  59. package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
  60. package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
  61. package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
  62. package/browser/addons/vrm/common/version.py +161 -0
  63. package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
  64. package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
  65. package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
  66. package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
  67. package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
  68. package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
  69. package/browser/addons/vrm/common/workspace.py +227 -0
  70. package/browser/addons/vrm/common/writable_context.py +77 -0
  71. package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
  72. package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
  73. package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
  74. package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
  75. package/browser/addons/vrm/documentation/en-us/index.md +41 -0
  76. package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
  77. package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
  78. package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
  79. package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
  80. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
  81. package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
  82. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
  83. package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
  84. package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
  85. package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
  86. package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
  87. package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
  88. package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
  89. package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
  90. package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
  91. package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
  92. package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
  93. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
  94. package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
  95. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
  96. package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
  97. package/browser/addons/vrm/editor/__init__.py +1 -0
  98. package/browser/addons/vrm/editor/extension.py +425 -0
  99. package/browser/addons/vrm/editor/extension_accessor.py +75 -0
  100. package/browser/addons/vrm/editor/handler.py +84 -0
  101. package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
  102. package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
  103. package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
  104. package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
  105. package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
  106. package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
  107. package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
  108. package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
  109. package/browser/addons/vrm/editor/make_armature.py +709 -0
  110. package/browser/addons/vrm/editor/menu.py +147 -0
  111. package/browser/addons/vrm/editor/migration.py +297 -0
  112. package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
  113. package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
  114. package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
  115. package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
  116. package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
  117. package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
  118. package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
  119. package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
  120. package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
  121. package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
  122. package/browser/addons/vrm/editor/ops.py +471 -0
  123. package/browser/addons/vrm/editor/panel.py +315 -0
  124. package/browser/addons/vrm/editor/property_group.py +888 -0
  125. package/browser/addons/vrm/editor/search.py +682 -0
  126. package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
  127. package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
  128. package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
  129. package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
  130. package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
  131. package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
  132. package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
  133. package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
  134. package/browser/addons/vrm/editor/subscription.py +123 -0
  135. package/browser/addons/vrm/editor/t_pose.py +713 -0
  136. package/browser/addons/vrm/editor/validation.py +1271 -0
  137. package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
  138. package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
  139. package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
  140. package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
  141. package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
  142. package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
  143. package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
  144. package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
  145. package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
  146. package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
  147. package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
  148. package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
  149. package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
  150. package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
  151. package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
  152. package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
  153. package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
  154. package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
  155. package/browser/addons/vrm/exporter/__init__.py +1 -0
  156. package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
  157. package/browser/addons/vrm/exporter/export_scene.py +930 -0
  158. package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
  159. package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
  160. package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
  161. package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
  162. package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
  163. package/browser/addons/vrm/external/__init__.py +1 -0
  164. package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
  165. package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
  166. package/browser/addons/vrm/external/rigify_support.py +1 -0
  167. package/browser/addons/vrm/importer/__init__.py +1 -0
  168. package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
  169. package/browser/addons/vrm/importer/file_handler.py +34 -0
  170. package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
  171. package/browser/addons/vrm/importer/import_scene.py +646 -0
  172. package/browser/addons/vrm/importer/license_validation.py +198 -0
  173. package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
  174. package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
  175. package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
  176. package/browser/addons/vrm/importer/vrm_diff.py +316 -0
  177. package/browser/addons/vrm/locale/__init__.py +1 -0
  178. package/browser/addons/vrm/locale/ja_jp.py +826 -0
  179. package/browser/addons/vrm/locale/ruff.toml +8 -0
  180. package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
  181. package/browser/addons/vrm/locale/zh_hans.py +928 -0
  182. package/browser/addons/vrm/registration.py +670 -0
  183. package/browser/addons/vrm.json +18 -0
  184. package/browser/blender-emscripten-engine.mts +66 -129
  185. package/browser/blender-engine.mts +20 -186
  186. package/browser/blender-wali-engine.mts +37 -81
  187. package/browser/describe-thrown.mts +3 -6
  188. package/browser/document-chunks.mts +4 -16
  189. package/browser/gltf_export.py +3161 -0
  190. package/browser/heap-growth.mts +23 -0
  191. package/browser/index.ts +3 -0
  192. package/browser/protocol.ts +130 -43
  193. package/browser/pull-job.mts +2 -10
  194. package/browser/rna.ts +190 -73
  195. package/browser/runtime.ts +271 -106
  196. package/browser/session-addons.mts +33 -0
  197. package/browser/session-frame.mts +5 -24
  198. package/browser/session.py +1389 -1446
  199. package/browser/three/attach-presenter.ts +11 -39
  200. package/browser/three/blender-base64.ts +1 -36
  201. package/browser/three/blender-corner-normals.ts +1 -23
  202. package/browser/three/blender-display-shaders.json +1 -1
  203. package/browser/three/blender-display-shaders.lut +0 -0
  204. package/browser/three/blender-display-transform.ts +2 -1
  205. package/browser/three/blender-frame-columns.ts +5 -13
  206. package/browser/three/blender-graph-material.ts +18 -3
  207. package/browser/three/blender-node-graph.ts +10 -34
  208. package/browser/three/blender-physical-material.ts +2 -31
  209. package/browser/three/blender-runtime-armature.ts +7 -60
  210. package/browser/three/blender-runtime-frame.ts +9 -23
  211. package/browser/three/blender-runtime-geometry.ts +22 -26
  212. package/browser/three/blender-runtime-lighting.ts +48 -178
  213. package/browser/three/blender-runtime-shadows.ts +187 -1
  214. package/browser/three/blender-runtime-skeleton.ts +53 -1
  215. package/browser/three/blender-runtime-view.ts +492 -306
  216. package/browser/three/blender-runtime-weights.ts +9 -72
  217. package/browser/three/blender-sky.ts +13 -33
  218. package/browser/three/blender-triangulate.ts +7 -35
  219. package/browser/three/blender-viewport-sampling.ts +235 -0
  220. package/browser/three/blender-workbench-material.ts +25 -29
  221. package/browser/three/blender-workbench-outline.ts +199 -0
  222. package/browser/three/blender-world-volume.ts +62 -10
  223. package/browser/three/presenter.ts +10 -26
  224. package/browser/three/release.ts +5 -21
  225. package/browser/three/sky-precompute-worker.ts +1 -18
  226. package/browser/three/sky-worker.ts +5 -23
  227. package/browser/three/world-field-sampler.ts +12 -26
  228. package/browser/volter_three.py +9 -105
  229. package/browser/worker.ts +195 -180
  230. package/package.json +5 -2
  231. package/wasm/BUNDLE.json +17 -17
  232. package/wasm/DEPENDENCY-LICENSES.txt +14 -0
  233. package/wasm/blender_browser.data.br +0 -0
  234. package/wasm/blender_browser.js +1 -1
  235. package/wasm/blender_browser.wasm.br +0 -0
  236. package/browser/frame-transfer.mts +0 -19
  237. package/browser/three/agx-base-srgb.lut +0 -0
  238. package/browser/three/agx-look-medium-high-contrast.lut +0 -0
  239. package/browser/three/agx-look-punchy.lut +0 -0
  240. package/browser/three/blender-agx.ts +0 -235
  241. package/browser/three/blender-display-lut.ts +0 -145
  242. package/browser/three/blender-filmic.ts +0 -49
  243. package/browser/three/blender-standard.ts +0 -68
  244. package/browser/three/blender-world-visibility.ts +0 -278
  245. package/browser/three/filmic-srgb.lut +0 -0
@@ -0,0 +1,891 @@
1
+ # SPDX-License-Identifier: MIT OR GPL-3.0-or-later
2
+
3
+ import itertools
4
+ import math
5
+ from collections.abc import Iterator, Mapping
6
+ from dataclasses import dataclass, field
7
+ from functools import cached_property
8
+ from typing import Final, Optional, Union
9
+
10
+ from bpy.types import Armature, Bone, Object
11
+ from mathutils import Matrix
12
+
13
+ from ..deep import Json
14
+ from ..logger import get_logger
15
+ from ..vrm1.human_bone import (
16
+ HumanBoneName,
17
+ HumanBoneSpecification,
18
+ HumanBoneSpecifications,
19
+ )
20
+
21
+ _logger = get_logger(__name__)
22
+
23
+
24
+ CENTRAL_BONE_NAMES = frozenset(
25
+ [
26
+ HumanBoneName.HEAD,
27
+ HumanBoneName.NECK,
28
+ HumanBoneName.UPPER_CHEST,
29
+ HumanBoneName.CHEST,
30
+ HumanBoneName.SPINE,
31
+ HumanBoneName.HIPS,
32
+ ]
33
+ )
34
+
35
+ HIPS_TO_SPINE_BONE_NAMES = frozenset(
36
+ [
37
+ HumanBoneName.HIPS,
38
+ HumanBoneName.SPINE,
39
+ ]
40
+ )
41
+
42
+ SPINE_TO_UPPER_CHEST_BONE_NAMES = frozenset(
43
+ [
44
+ HumanBoneName.SPINE,
45
+ HumanBoneName.CHEST,
46
+ HumanBoneName.UPPER_CHEST,
47
+ ]
48
+ )
49
+
50
+ SPINE_TO_NECK_NAMES = frozenset(
51
+ [
52
+ HumanBoneName.SPINE,
53
+ HumanBoneName.CHEST,
54
+ HumanBoneName.UPPER_CHEST,
55
+ HumanBoneName.NECK,
56
+ ]
57
+ )
58
+
59
+ LEFT_ARM_BONE_NAMES = frozenset(
60
+ [
61
+ HumanBoneName.LEFT_EYE,
62
+ HumanBoneName.LEFT_HAND,
63
+ HumanBoneName.LEFT_SHOULDER,
64
+ HumanBoneName.LEFT_UPPER_ARM,
65
+ HumanBoneName.LEFT_LOWER_ARM,
66
+ ]
67
+ )
68
+
69
+ LEFT_LEG_BONE_NAMES = frozenset(
70
+ [
71
+ HumanBoneName.LEFT_UPPER_LEG,
72
+ HumanBoneName.LEFT_LOWER_LEG,
73
+ HumanBoneName.LEFT_FOOT,
74
+ ]
75
+ )
76
+
77
+ RIGHT_ARM_BONE_NAMES = frozenset(
78
+ [
79
+ HumanBoneName.RIGHT_EYE,
80
+ HumanBoneName.RIGHT_HAND,
81
+ HumanBoneName.RIGHT_SHOULDER,
82
+ HumanBoneName.RIGHT_UPPER_ARM,
83
+ HumanBoneName.RIGHT_LOWER_ARM,
84
+ ]
85
+ )
86
+
87
+ RIGHT_LEG_BONE_NAMES = frozenset(
88
+ [
89
+ HumanBoneName.RIGHT_UPPER_LEG,
90
+ HumanBoneName.RIGHT_LOWER_LEG,
91
+ HumanBoneName.RIGHT_FOOT,
92
+ ]
93
+ )
94
+
95
+
96
+ @dataclass
97
+ class Counter:
98
+ count: int = 0
99
+ cache_hit: int = 0
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class NormalizedBone:
104
+ name: str
105
+ x: float
106
+ y: float
107
+ z: float
108
+ children: tuple["NormalizedBone", ...]
109
+ recursive_len: int
110
+
111
+ @classmethod
112
+ def create(
113
+ cls,
114
+ armature_matrix: Matrix,
115
+ max_x: float,
116
+ max_y: float,
117
+ max_z: float,
118
+ bone: Bone,
119
+ ) -> "NormalizedBone":
120
+ x, y, z = (armature_matrix @ bone.matrix_local).translation
121
+ normalized_x = math.copysign(math.sqrt(abs(x) / max_x), x) if max_x > 0 else 0
122
+ normalized_y = math.copysign(math.sqrt(abs(y) / max_y), y) if max_y > 0 else 0
123
+ normalized_z = math.copysign(math.sqrt(abs(z) / max_z), z) if max_z > 0 else 0
124
+ children = tuple(
125
+ cls.create(armature_matrix, max_x, max_y, max_z, child)
126
+ for child in bone.children
127
+ )
128
+ return NormalizedBone(
129
+ name=bone.name,
130
+ x=normalized_x,
131
+ y=normalized_y,
132
+ z=normalized_z,
133
+ children=children,
134
+ recursive_len=1 + sum(child.recursive_len for child in children),
135
+ )
136
+
137
+ @classmethod
138
+ def from_json(cls, data: Json) -> "NormalizedBone":
139
+ if not isinstance(data, Mapping):
140
+ message = "NormalizedBone must be an object"
141
+ raise TypeError(message)
142
+ name = data.get("name")
143
+ if not isinstance(name, str):
144
+ message = "NormalizedBone.name must be a string"
145
+ raise TypeError(message)
146
+
147
+ x = data.get("x")
148
+ y = data.get("y")
149
+ z = data.get("z")
150
+ if not isinstance(x, (int, float)):
151
+ message = "NormalizedBone.x must be a number"
152
+ raise TypeError(message)
153
+ if not isinstance(y, (int, float)):
154
+ message = "NormalizedBone.y must be a number"
155
+ raise TypeError(message)
156
+ if not isinstance(z, (int, float)):
157
+ message = "NormalizedBone.z must be a number"
158
+ raise TypeError(message)
159
+
160
+ children_data = data.get("children")
161
+ if children_data is None:
162
+ children_data = []
163
+ if not isinstance(children_data, list):
164
+ message = "NormalizedBone.children must be a list"
165
+ raise TypeError(message)
166
+ children = tuple(cls.from_json(child) for child in children_data)
167
+
168
+ recursive_len = data.get("recursiveLen")
169
+ if recursive_len is None:
170
+ recursive_len_value = 1 + sum(child.recursive_len for child in children)
171
+ elif isinstance(recursive_len, int):
172
+ recursive_len_value = recursive_len
173
+ else:
174
+ message = "NormalizedBone.recursiveLen must be an int"
175
+ raise ValueError(message)
176
+
177
+ return NormalizedBone(
178
+ name=name,
179
+ x=float(x),
180
+ y=float(y),
181
+ z=float(z),
182
+ children=children,
183
+ recursive_len=recursive_len_value,
184
+ )
185
+
186
+ def to_json(self) -> Json:
187
+ return {
188
+ "name": self.name,
189
+ "x": self.x,
190
+ "y": self.y,
191
+ "z": self.z,
192
+ "children": [child.to_json() for child in self.children],
193
+ "recursiveLen": self.recursive_len,
194
+ }
195
+
196
+ def dump(self) -> str:
197
+ output = f'("{self.name}", ({self.x:.3f}, {self.y:.3f}, {self.z:.3f})): {{\n'
198
+ for child in self.children:
199
+ output += child.dump()
200
+ output += "},\n"
201
+ return output
202
+
203
+
204
+ SearchBranch = Union["UnassignedSearchBranch", "AssignedSearchBranch"]
205
+
206
+
207
+ def _parent_from_json(
208
+ data: Json,
209
+ ) -> Optional[tuple[NormalizedBone, HumanBoneSpecification]]:
210
+ if data is None:
211
+ return None
212
+ if not isinstance(data, Mapping):
213
+ message = "parent must be an object or null"
214
+ raise TypeError(message)
215
+ bone_data = data.get("bone")
216
+ if not isinstance(bone_data, Mapping):
217
+ message = "parent.bone must be an object"
218
+ raise TypeError(message)
219
+ bone = NormalizedBone.from_json(bone_data)
220
+ human_bone_name_str = data.get("humanBoneName")
221
+ if not isinstance(human_bone_name_str, str):
222
+ message = "parent.humanBoneName must be a string"
223
+ raise TypeError(message)
224
+ human_bone_specification = HumanBoneSpecifications.from_name_str(
225
+ human_bone_name_str
226
+ )
227
+ if human_bone_specification is None:
228
+ message = f"Unknown human bone name: {human_bone_name_str}"
229
+ raise ValueError(message)
230
+ return (bone, human_bone_specification)
231
+
232
+
233
+ def _search_branch_from_json(data: Json) -> SearchBranch:
234
+ if not isinstance(data, Mapping):
235
+ message = "SearchBranch must be an object"
236
+ raise TypeError(message)
237
+ type_name = data.get("type_name")
238
+ if type_name == AssignedSearchBranch.__name__:
239
+ return AssignedSearchBranch.from_json(data)
240
+
241
+ if type_name == UnassignedSearchBranch.__name__:
242
+ return UnassignedSearchBranch.from_json(data)
243
+
244
+ message = (
245
+ "SearchBranch.type_name must be 'AssignedSearchBranch'"
246
+ + " or 'UnassignedSearchBranch'"
247
+ )
248
+ raise ValueError(message)
249
+
250
+
251
+ @dataclass(frozen=True)
252
+ class UnassignedSearchBranch:
253
+ children: tuple[SearchBranch, ...] = ()
254
+
255
+ def to_json(self) -> Json:
256
+ return {
257
+ "type_name": type(self).__name__,
258
+ "children": [child.to_json() for child in self.children],
259
+ }
260
+
261
+ @classmethod
262
+ def from_json(cls, data: Mapping[str, Json]) -> "UnassignedSearchBranch":
263
+ children_data = data.get("children")
264
+ if children_data is None:
265
+ children_data = []
266
+ if not isinstance(children_data, list):
267
+ message = "UnassignedSearchBranch.children must be a list"
268
+ raise TypeError(message)
269
+ return UnassignedSearchBranch(
270
+ children=tuple(_search_branch_from_json(child) for child in children_data)
271
+ )
272
+
273
+ @cached_property
274
+ def score(self) -> float:
275
+ return sum((child.score for child in self.children), start=0.0)
276
+
277
+ @cached_property
278
+ def recursive_mappings(self) -> Mapping[HumanBoneSpecification, NormalizedBone]:
279
+ result: dict[HumanBoneSpecification, NormalizedBone] = {}
280
+ for child in self.children:
281
+ result.update(child.recursive_mappings)
282
+ return result
283
+
284
+ def __lt__(self, other: "SearchBranch") -> bool:
285
+ return self.score < other.score
286
+
287
+
288
+ @dataclass(frozen=True)
289
+ class AssignedSearchBranch:
290
+ depth: int
291
+ bone: NormalizedBone
292
+ human_bone_specification: HumanBoneSpecification
293
+ parent: Optional[tuple[NormalizedBone, HumanBoneSpecification]]
294
+ children: tuple[SearchBranch, ...]
295
+
296
+ def to_json(self) -> Json:
297
+ parent = self.parent
298
+ if parent is None:
299
+ parent_json: Json = None
300
+ else:
301
+ parent_bone, parent_spec = parent
302
+ parent_json = {
303
+ "bone": parent_bone.to_json(),
304
+ "humanBoneName": parent_spec.name.value,
305
+ }
306
+ return {
307
+ "type_name": type(self).__name__,
308
+ "depth": self.depth,
309
+ "bone": self.bone.to_json(),
310
+ "humanBoneName": self.human_bone_specification.name.value,
311
+ "parent": parent_json,
312
+ "children": [child.to_json() for child in self.children],
313
+ }
314
+
315
+ @classmethod
316
+ def from_json(cls, data: Mapping[str, Json]) -> "AssignedSearchBranch":
317
+ depth = data.get("depth")
318
+ if not isinstance(depth, int):
319
+ message = "AssignedSearchBranch.depth must be an int"
320
+ raise TypeError(message)
321
+ bone_data = data.get("bone")
322
+ if not isinstance(bone_data, Mapping):
323
+ message = "AssignedSearchBranch.bone must be an object"
324
+ raise TypeError(message)
325
+ bone = NormalizedBone.from_json(bone_data)
326
+ human_bone_name_str = data.get("humanBoneName")
327
+ if not isinstance(human_bone_name_str, str):
328
+ message = "AssignedSearchBranch.humanBoneName must be a string"
329
+ raise TypeError(message)
330
+ human_bone_specification = HumanBoneSpecifications.from_name_str(
331
+ human_bone_name_str
332
+ )
333
+ if human_bone_specification is None:
334
+ message = f"Unknown human bone name: {human_bone_name_str}"
335
+ raise ValueError(message)
336
+ parent = _parent_from_json(data.get("parent"))
337
+ children_data = data.get("children")
338
+ if children_data is None:
339
+ children_data = []
340
+ if not isinstance(children_data, list):
341
+ message = "AssignedSearchBranch.children must be a list"
342
+ raise TypeError(message)
343
+ children = tuple(_search_branch_from_json(child) for child in children_data)
344
+ return AssignedSearchBranch(
345
+ depth=depth,
346
+ bone=bone,
347
+ human_bone_specification=human_bone_specification,
348
+ parent=parent,
349
+ children=children,
350
+ )
351
+
352
+ @cached_property
353
+ def score(self) -> float:
354
+ total_score = sum((child.score for child in self.children), start=0.0)
355
+
356
+ total_score -= math.sqrt(self.depth)
357
+
358
+ if self.human_bone_specification.requirement:
359
+ total_score += 1000
360
+ else:
361
+ total_score += 20
362
+
363
+ human_bone_name = self.human_bone_specification.name
364
+
365
+ if self.bone.z > 0:
366
+ total_score += 5
367
+
368
+ if human_bone_name in CENTRAL_BONE_NAMES:
369
+ total_score += self.bone.z - abs(self.bone.x)
370
+ elif human_bone_name in LEFT_ARM_BONE_NAMES:
371
+ total_score += self.bone.x / 2
372
+ elif human_bone_name in LEFT_LEG_BONE_NAMES:
373
+ total_score += 0.2 if self.bone.x > 0 else 0
374
+ elif human_bone_name in RIGHT_ARM_BONE_NAMES:
375
+ total_score += -self.bone.x / 2
376
+ elif human_bone_name in RIGHT_LEG_BONE_NAMES:
377
+ total_score += 0.2 if self.bone.x < 0 else 0
378
+
379
+ parent = self.parent
380
+ if parent is None:
381
+ return total_score
382
+
383
+ parent_bone, parent_human_bone_specification = parent
384
+ human_bone_name = self.human_bone_specification.name
385
+ parent_human_bone_name = parent_human_bone_specification.name
386
+ if human_bone_name == HumanBoneName.SPINE:
387
+ if (
388
+ parent_human_bone_name == HumanBoneName.HIPS
389
+ and parent_bone.z < self.bone.z
390
+ ):
391
+ total_score += 5
392
+ elif human_bone_name == HumanBoneName.CHEST:
393
+ if (
394
+ parent_human_bone_name in HIPS_TO_SPINE_BONE_NAMES
395
+ and parent_bone.z < self.bone.z
396
+ ):
397
+ total_score += 5
398
+ elif human_bone_name == HumanBoneName.UPPER_CHEST:
399
+ if (
400
+ parent_human_bone_name == HumanBoneName.CHEST
401
+ and parent_bone.z < self.bone.z
402
+ ):
403
+ total_score += 5
404
+ elif human_bone_name == HumanBoneName.NECK:
405
+ if (
406
+ parent_human_bone_name in SPINE_TO_UPPER_CHEST_BONE_NAMES
407
+ and parent_bone.z < self.bone.z
408
+ ):
409
+ total_score += 5
410
+ elif human_bone_name == HumanBoneName.HEAD:
411
+ if (
412
+ parent_human_bone_name in SPINE_TO_NECK_NAMES
413
+ and parent_bone.z < self.bone.z
414
+ ):
415
+ total_score += 5
416
+ else:
417
+ pass
418
+
419
+ return total_score
420
+
421
+ @cached_property
422
+ def recursive_mappings(self) -> Mapping[HumanBoneSpecification, NormalizedBone]:
423
+ result: dict[HumanBoneSpecification, NormalizedBone] = {
424
+ self.human_bone_specification: self.bone
425
+ }
426
+ for child in self.children:
427
+ result.update(child.recursive_mappings)
428
+ return result
429
+
430
+ def __lt__(self, other: "SearchBranch") -> bool:
431
+ return self.score < other.score
432
+
433
+
434
+ SEARCH_STOP_BRANCH: Final[SearchBranch] = UnassignedSearchBranch()
435
+
436
+
437
+ @dataclass(frozen=True)
438
+ class SearchContext:
439
+ max_search_count: int
440
+ require_requirement: int
441
+ only_requirement: bool
442
+ counter: Counter = field(default_factory=Counter)
443
+ cache: dict[
444
+ tuple[tuple[str, ...], tuple[str, ...], Optional[tuple[str, str]], int],
445
+ SearchBranch,
446
+ ] = field(
447
+ default_factory=dict[
448
+ tuple[tuple[str, ...], tuple[str, ...], Optional[tuple[str, str]], int],
449
+ SearchBranch,
450
+ ]
451
+ )
452
+ max_recursion_depth: int = 50
453
+
454
+
455
+ DEFAULT_MAX_SEARCH_COUNT: Final[int] = 1000000
456
+
457
+
458
+ def create_structure_based_mapping(
459
+ armature: Object,
460
+ human_bone_specification: HumanBoneSpecification = HumanBoneSpecifications.HIPS,
461
+ max_search_count: int = DEFAULT_MAX_SEARCH_COUNT,
462
+ ) -> Mapping[str, HumanBoneSpecification]:
463
+ if not isinstance(armature_data := armature.data, Armature):
464
+ message = f"{type(armature.data)} is not an Armature"
465
+ raise TypeError(message)
466
+
467
+ pose_position = armature_data.pose_position
468
+ armature_data.pose_position = "REST"
469
+
470
+ translations = [
471
+ (armature.matrix_world @ bone.matrix_local).translation
472
+ for bone in armature_data.bones
473
+ ]
474
+ max_x = max((abs(translation.x) for translation in translations), default=0)
475
+ max_y = max((abs(translation.y) for translation in translations), default=0)
476
+ max_z = max((abs(translation.z) for translation in translations), default=0)
477
+
478
+ root_bones = [
479
+ NormalizedBone.create(
480
+ armature.matrix_world,
481
+ max_x,
482
+ max_y,
483
+ max_z,
484
+ bone,
485
+ )
486
+ for bone in armature_data.bones
487
+ if bone.parent is None
488
+ ]
489
+ armature_data.pose_position = pose_position
490
+ counter = Counter()
491
+ max_result: SearchBranch = SEARCH_STOP_BRANCH
492
+
493
+ for require_requirement, only_requirement in ((1, True), (0, False)):
494
+ search_context = SearchContext(
495
+ require_requirement=require_requirement,
496
+ only_requirement=only_requirement,
497
+ max_search_count=max_search_count,
498
+ counter=counter,
499
+ )
500
+ for skip_count in range(1000):
501
+ if counter.count > search_context.max_search_count:
502
+ break
503
+ changed = False
504
+ for root_bone in root_bones:
505
+ if counter.count > search_context.max_search_count:
506
+ break
507
+ if (
508
+ root_bone.recursive_len
509
+ < skip_count
510
+ + HumanBoneSpecifications.HIPS.recursive_requirement_len
511
+ * require_requirement
512
+ ):
513
+ break
514
+ result = _search_structure_based_mapping_step(
515
+ root_bone,
516
+ (human_bone_specification,),
517
+ None,
518
+ 1,
519
+ skip_count,
520
+ search_context,
521
+ )
522
+ if max_result < result:
523
+ max_result = result
524
+ changed = True
525
+
526
+ if not changed:
527
+ continue
528
+
529
+ _logger.warning("Structure search context:")
530
+ _logger.warning(" Search Count Limit: %d", search_context.max_search_count)
531
+ _logger.warning(" Recursion Limit: %d", search_context.max_recursion_depth)
532
+ _logger.warning(" Skip Count: %d", skip_count)
533
+ _logger.warning(
534
+ " Require Requirement: %d", search_context.require_requirement
535
+ )
536
+ _logger.warning(" Only Requirement: %d", search_context.only_requirement)
537
+ _logger.warning(
538
+ "Structure search: Total searches: %d, Cache hits: %d",
539
+ counter.count,
540
+ counter.cache_hit,
541
+ )
542
+ _logger.warning(
543
+ "Generated mappings:\n %s",
544
+ "\n ".join(
545
+ f"{spec.name.value}: {bone.name}"
546
+ for spec, bone in max_result.recursive_mappings.items()
547
+ ),
548
+ )
549
+
550
+ _logger.warning(
551
+ "Structure search completed:"
552
+ " Total searches: %d (soft limit: %d), Cache hits: %d",
553
+ counter.count,
554
+ max_search_count,
555
+ counter.cache_hit,
556
+ )
557
+ mapping = max_result.recursive_mappings
558
+ return {spec.name: bone for bone, spec in mapping.items()}
559
+
560
+
561
+ def _search_structure_based_mapping_step(
562
+ bone: NormalizedBone,
563
+ human_bone_specifications: tuple[HumanBoneSpecification, ...],
564
+ parent: Optional[tuple[NormalizedBone, HumanBoneSpecification]],
565
+ depth: int,
566
+ skip_count: int,
567
+ search_context: SearchContext,
568
+ ) -> SearchBranch:
569
+ if skip_count < 0 or bone.recursive_len < skip_count:
570
+ return SEARCH_STOP_BRANCH
571
+
572
+ if search_context.require_requirement:
573
+ recursive_requirement_len = sum(
574
+ human_bone_specification.recursive_requirement_len
575
+ for human_bone_specification in human_bone_specifications
576
+ )
577
+ if bone.recursive_len < skip_count + recursive_requirement_len:
578
+ return SEARCH_STOP_BRANCH
579
+
580
+ if depth > search_context.max_recursion_depth:
581
+ return SEARCH_STOP_BRANCH
582
+
583
+ counter = search_context.counter
584
+ counter.count += 1
585
+ if counter.count > search_context.max_search_count:
586
+ return SEARCH_STOP_BRANCH
587
+
588
+ cache_key = (
589
+ (bone.name,),
590
+ tuple(spec.name.value for spec in human_bone_specifications),
591
+ None
592
+ if parent is None
593
+ else (
594
+ parent[0].name,
595
+ parent[1].name.value,
596
+ ),
597
+ skip_count,
598
+ )
599
+ cache = search_context.cache
600
+ if (search_branchs := cache.get(cache_key)) is not None:
601
+ counter.cache_hit += 1
602
+ return search_branchs
603
+
604
+ result = SEARCH_STOP_BRANCH
605
+
606
+ if len(human_bone_specifications) == 1:
607
+ human_bone_specification = human_bone_specifications[0]
608
+ human_bone_specification_children = (
609
+ human_bone_specification.required_children
610
+ if search_context.only_requirement
611
+ else human_bone_specification.children
612
+ )
613
+ if bone.children and human_bone_specification_children:
614
+ children = (
615
+ _search_structure_based_mapping_product(
616
+ bone.children,
617
+ human_bone_specification_children,
618
+ (bone, human_bone_specification),
619
+ depth + 1,
620
+ skip_count,
621
+ search_context,
622
+ ),
623
+ )
624
+ else:
625
+ children = ()
626
+
627
+ result = max(
628
+ result,
629
+ AssignedSearchBranch(
630
+ depth=depth + 1,
631
+ bone=bone,
632
+ human_bone_specification=human_bone_specification,
633
+ parent=parent,
634
+ children=children,
635
+ ),
636
+ )
637
+
638
+ if skip_count > 0:
639
+ result = max(
640
+ result,
641
+ _search_structure_based_mapping_product(
642
+ bone.children,
643
+ human_bone_specifications,
644
+ parent,
645
+ depth + 1,
646
+ skip_count - 1,
647
+ search_context,
648
+ ),
649
+ )
650
+
651
+ cache[cache_key] = result
652
+ return result
653
+
654
+
655
+ def _search_structure_based_mapping_product(
656
+ bones: tuple[NormalizedBone, ...],
657
+ human_bone_specifications: tuple[HumanBoneSpecification, ...],
658
+ parent: Optional[tuple[NormalizedBone, HumanBoneSpecification]],
659
+ depth: int,
660
+ skip_count: int,
661
+ search_context: SearchContext,
662
+ ) -> SearchBranch:
663
+ if not bones or not human_bone_specifications:
664
+ return SEARCH_STOP_BRANCH
665
+
666
+ if depth > search_context.max_recursion_depth:
667
+ return SEARCH_STOP_BRANCH
668
+
669
+ if skip_count < 0 or sum(bone.recursive_len for bone in bones) < skip_count:
670
+ return SEARCH_STOP_BRANCH
671
+
672
+ counter = search_context.counter
673
+ counter.count += 1
674
+ if counter.count > search_context.max_search_count:
675
+ return SEARCH_STOP_BRANCH
676
+
677
+ cache = search_context.cache
678
+ cache_key = (
679
+ tuple(sorted(bone.name for bone in bones)),
680
+ tuple(spec.name.value for spec in human_bone_specifications),
681
+ None
682
+ if parent is None
683
+ else (
684
+ parent[0].name,
685
+ parent[1].name.value,
686
+ ),
687
+ skip_count,
688
+ )
689
+ if (search_branchs := cache.get(cache_key)) is not None:
690
+ counter.cache_hit += 1
691
+ return search_branchs
692
+
693
+ result = SEARCH_STOP_BRANCH
694
+
695
+ for skipping_human_bone_specification in human_bone_specifications:
696
+ if skipping_human_bone_specification.requirement:
697
+ continue
698
+ skipping_children = sum(
699
+ tuple(
700
+ skipping_human_bone_specification.children
701
+ if human_bone_specification == skipping_human_bone_specification
702
+ else (human_bone_specification,)
703
+ for human_bone_specification in human_bone_specifications
704
+ ),
705
+ (),
706
+ )
707
+ if skipping_children:
708
+ result = max(
709
+ result,
710
+ _search_structure_based_mapping_product(
711
+ bones,
712
+ skipping_children,
713
+ parent,
714
+ depth + 1,
715
+ skip_count,
716
+ search_context,
717
+ ),
718
+ )
719
+
720
+ assignment_combination_len = min(len(human_bone_specifications), len(bones))
721
+ if (
722
+ not search_context.require_requirement
723
+ or sum(int(s.requirement) for s in human_bone_specifications)
724
+ <= assignment_combination_len
725
+ ):
726
+ spec_tuples = tuple(
727
+ (human_bone_specification,)
728
+ for human_bone_specification in human_bone_specifications
729
+ )
730
+ for spec_children, bone_children in itertools.product(
731
+ itertools.combinations(spec_tuples, assignment_combination_len),
732
+ itertools.combinations(bones, assignment_combination_len),
733
+ ):
734
+ for spec_children_permutation in itertools.permutations(spec_children):
735
+ for bone_packings in _iter_skip_counts_and_bone_combinations(
736
+ skip_count,
737
+ tuple(
738
+ zip(
739
+ bone_children,
740
+ (
741
+ sum(s.recursive_requirement_len for s in ss)
742
+ * search_context.require_requirement
743
+ for ss in spec_children_permutation
744
+ ),
745
+ )
746
+ ),
747
+ ):
748
+ children = tuple(
749
+ _search_structure_based_mapping_step(
750
+ bone_child,
751
+ spec_child,
752
+ parent,
753
+ depth + 1,
754
+ child_skip_count,
755
+ search_context,
756
+ )
757
+ for spec_child, (bone_child, child_skip_count) in zip(
758
+ spec_children_permutation, bone_packings
759
+ )
760
+ )
761
+ result = max(result, UnassignedSearchBranch(children=children))
762
+ if counter.count > search_context.max_search_count:
763
+ break
764
+
765
+ for special_group in (
766
+ (
767
+ HumanBoneSpecifications.LEFT_UPPER_LEG,
768
+ HumanBoneSpecifications.RIGHT_UPPER_LEG,
769
+ ),
770
+ (
771
+ HumanBoneSpecifications.LEFT_UPPER_ARM,
772
+ HumanBoneSpecifications.RIGHT_UPPER_ARM,
773
+ ),
774
+ (
775
+ HumanBoneSpecifications.LEFT_EYE,
776
+ HumanBoneSpecifications.RIGHT_EYE,
777
+ ),
778
+ # (HumanBoneSpecifications.LEFT_THUMB_METACARPAL,),
779
+ # (HumanBoneSpecifications.RIGHT_THUMB_METACARPAL,),
780
+ ):
781
+ if not all(s in human_bone_specifications for s in special_group):
782
+ continue
783
+
784
+ remaining_group = [
785
+ s for s in human_bone_specifications if s not in special_group
786
+ ]
787
+
788
+ if remaining_group:
789
+ groups = (
790
+ special_group,
791
+ *tuple((remaining,) for remaining in remaining_group),
792
+ )
793
+ else:
794
+ groups = (special_group,)
795
+
796
+ special_assignment_combination_len = min(len(groups), len(bones))
797
+ if (
798
+ search_context.require_requirement
799
+ and special_assignment_combination_len
800
+ < sum(int(any(s.requirement for s in group)) for group in groups)
801
+ ):
802
+ break
803
+
804
+ for spec_children, bone_children in itertools.product(
805
+ itertools.combinations(groups, special_assignment_combination_len),
806
+ itertools.combinations(bones, special_assignment_combination_len),
807
+ ):
808
+ for spec_children_permutation in itertools.permutations(spec_children):
809
+ for bone_packings in _iter_skip_counts_and_bone_combinations(
810
+ skip_count,
811
+ tuple(
812
+ zip(
813
+ bone_children,
814
+ (
815
+ sum(s.recursive_requirement_len for s in ss)
816
+ * search_context.require_requirement
817
+ for ss in spec_children_permutation
818
+ ),
819
+ )
820
+ ),
821
+ ):
822
+ children = tuple(
823
+ _search_structure_based_mapping_step(
824
+ bone_child,
825
+ spec_child,
826
+ parent,
827
+ depth + 1,
828
+ child_skip_count,
829
+ search_context,
830
+ )
831
+ for spec_child, (bone_child, child_skip_count) in zip(
832
+ spec_children_permutation, bone_packings
833
+ )
834
+ )
835
+ result = max(result, UnassignedSearchBranch(children=children))
836
+ if counter.count > search_context.max_search_count:
837
+ break
838
+ break
839
+
840
+ cache[cache_key] = result
841
+ return result
842
+
843
+
844
+ def _backtrack_skip_counts_and_bone_combinations(
845
+ *,
846
+ bones: tuple[tuple[NormalizedBone, int], ...],
847
+ counts: list[int],
848
+ bone_index: int,
849
+ remaining: int,
850
+ ) -> Iterator[list[tuple[NormalizedBone, int]]]:
851
+ if bone_index == len(bones):
852
+ if remaining == 0:
853
+ yield [(bone, count) for (bone, _), count in zip(bones, counts)]
854
+ return
855
+
856
+ bone, requirement_len = bones[bone_index]
857
+ max_count = min(bone.recursive_len - requirement_len, remaining)
858
+ for count in range(max_count, -1, -1):
859
+ counts.append(count)
860
+ yield from _backtrack_skip_counts_and_bone_combinations(
861
+ bones=bones,
862
+ counts=counts,
863
+ bone_index=bone_index + 1,
864
+ remaining=remaining - count,
865
+ )
866
+ counts.pop()
867
+
868
+
869
+ def _iter_skip_counts_and_bone_combinations(
870
+ skip_count: int, bones: tuple[tuple[NormalizedBone, int], ...]
871
+ ) -> Iterator[list[tuple[NormalizedBone, int]]]:
872
+ if skip_count < 0:
873
+ return
874
+
875
+ if not bones:
876
+ if skip_count == 0:
877
+ yield []
878
+ return
879
+
880
+ total_recursive_len = sum(
881
+ b.recursive_len - requirement_len for b, requirement_len in bones
882
+ )
883
+ if total_recursive_len < skip_count:
884
+ return
885
+
886
+ yield from _backtrack_skip_counts_and_bone_combinations(
887
+ bones=bones,
888
+ counts=[],
889
+ bone_index=0,
890
+ remaining=skip_count,
891
+ )