@hypersoniclabs/helix-cli 0.1.7 → 0.1.23

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 (1715) hide show
  1. package/README.md +627 -6
  2. package/assets/bridge/metahuman-eye-baked.png +0 -0
  3. package/assets/item-def-sounds/Common/A_DryShot.ogg +0 -0
  4. package/assets/item-def-sounds/Common/A_LMG_CloseCoverA.ogg +0 -0
  5. package/assets/item-def-sounds/Common/A_LMG_CloseCoverB.ogg +0 -0
  6. package/assets/item-def-sounds/Common/A_LMG_InsertMag.ogg +0 -0
  7. package/assets/item-def-sounds/Common/A_LMG_RemoveMag.ogg +0 -0
  8. package/assets/item-def-sounds/Common/A_Pistol_InsertMag_001.ogg +0 -0
  9. package/assets/item-def-sounds/Common/A_Pistol_InsertMag_002.ogg +0 -0
  10. package/assets/item-def-sounds/Common/A_Pistol_InsertMag_003.ogg +0 -0
  11. package/assets/item-def-sounds/Common/A_Pistol_RemoveMag_001.ogg +0 -0
  12. package/assets/item-def-sounds/Common/A_Pistol_RemoveMag_002.ogg +0 -0
  13. package/assets/item-def-sounds/Common/A_Pistol_RemoveMag_003.ogg +0 -0
  14. package/assets/item-def-sounds/Common/A_Rifle_ChargingHandle_001.ogg +0 -0
  15. package/assets/item-def-sounds/Common/A_Rifle_ChargingHandle_002.ogg +0 -0
  16. package/assets/item-def-sounds/Common/A_Rifle_ChargingHandle_003.ogg +0 -0
  17. package/assets/item-def-sounds/Common/A_Rifle_InsertMag_001.ogg +0 -0
  18. package/assets/item-def-sounds/Common/A_Rifle_InsertMag_002.ogg +0 -0
  19. package/assets/item-def-sounds/Common/A_Rifle_InsertMag_003.ogg +0 -0
  20. package/assets/item-def-sounds/Common/A_Rifle_RemoveMag_001.ogg +0 -0
  21. package/assets/item-def-sounds/Common/A_Rifle_RemoveMag_002.ogg +0 -0
  22. package/assets/item-def-sounds/Common/A_Rifle_RemoveMag_003.ogg +0 -0
  23. package/assets/item-def-sounds/Common/A_SMG_ChargingHandle_001.ogg +0 -0
  24. package/assets/item-def-sounds/Common/A_SMG_ChargingHandle_002.ogg +0 -0
  25. package/assets/item-def-sounds/Common/A_SMG_ChargingHandle_003.ogg +0 -0
  26. package/assets/item-def-sounds/Common/A_Sniper_ChargingHandle.ogg +0 -0
  27. package/assets/item-def-sounds/Common/A_Sniper_RemoveMag_001.ogg +0 -0
  28. package/assets/item-def-sounds/Common/A_Sniper_RemoveMag_002.ogg +0 -0
  29. package/assets/item-def-sounds/Common/A_Sniper_RemoveMag_003.ogg +0 -0
  30. package/assets/item-def-sounds/Explosions/Explosions_Grenade_Punch-Close_01.ogg +0 -0
  31. package/assets/item-def-sounds/Explosions/Explosions_Grenade_Punch-Close_02.ogg +0 -0
  32. package/assets/item-def-sounds/Explosions/Explosions_Grenade_Punch-Close_03.ogg +0 -0
  33. package/assets/item-def-sounds/LMG/A_LWS-32_Shot_001.ogg +0 -0
  34. package/assets/item-def-sounds/LMG/A_LWS-32_Shot_002.ogg +0 -0
  35. package/assets/item-def-sounds/LMG/A_LWS-32_Shot_003.ogg +0 -0
  36. package/assets/item-def-sounds/Pistol/A_Gaston_Shot_001.ogg +0 -0
  37. package/assets/item-def-sounds/Pistol/A_Gaston_Shot_002.ogg +0 -0
  38. package/assets/item-def-sounds/Pistol/A_Gaston_Shot_003.ogg +0 -0
  39. package/assets/item-def-sounds/Rifle/A_KAL_Shot_001.ogg +0 -0
  40. package/assets/item-def-sounds/Rifle/A_KAL_Shot_002.ogg +0 -0
  41. package/assets/item-def-sounds/Rifle/A_KAL_Shot_003.ogg +0 -0
  42. package/assets/item-def-sounds/SMG/A_Bison_InsertMag.ogg +0 -0
  43. package/assets/item-def-sounds/SMG/A_Bison_RemoveMag.ogg +0 -0
  44. package/assets/item-def-sounds/SMG/A_Bison_Shot_001.ogg +0 -0
  45. package/assets/item-def-sounds/SMG/A_Bison_Shot_002.ogg +0 -0
  46. package/assets/item-def-sounds/SMG/A_Bison_Shot_003.ogg +0 -0
  47. package/assets/item-def-sounds/Shotgun/A_DB-12_Shot_001.ogg +0 -0
  48. package/assets/item-def-sounds/Shotgun/A_DB-12_Shot_002.ogg +0 -0
  49. package/assets/item-def-sounds/Shotgun/A_DB-12_Shot_003.ogg +0 -0
  50. package/assets/item-def-sounds/Sniper/A_CS-446_Shot_001.ogg +0 -0
  51. package/assets/item-def-sounds/Sniper/A_CS-446_Shot_002.ogg +0 -0
  52. package/assets/item-def-sounds/Sniper/A_CS-446_Shot_003.ogg +0 -0
  53. package/bridge-plugins/helix.bridge.unreal/bin/unreal-skeletal-mesh-extract.js +812 -0
  54. package/bridge-plugins/helix.bridge.unreal/bridge-plugin.json +40 -0
  55. package/bridge-plugins/helix.bridge.unreal/test/exporter.test.js +418 -0
  56. package/bridge-plugins/helix.bridge.unreal/test/handler.test.js +417 -0
  57. package/bridge-plugins/helix.bridge.unreal/unreal/export_skeletal_mesh.py +384 -0
  58. package/bridge-recipes/cyberpunk/cyberpunk-female-bone-map.json +476 -0
  59. package/bridge-recipes/sims4/README.md +43 -0
  60. package/bridge-recipes/sims4/sims4-female-bone-map.json +65 -0
  61. package/bridge-recipes/unreal/character-customizer/README.md +58 -0
  62. package/bridge-recipes/unreal/character-customizer/all.json +13 -0
  63. package/bridge-recipes/unreal/character-customizer/base-male-body.json +6 -0
  64. package/bridge-recipes/unreal/character-customizer/base-male-head.json +6 -0
  65. package/bridge-recipes/unreal/character-customizer/beard.addon.json +1 -0
  66. package/bridge-recipes/unreal/character-customizer/beard.json +6 -0
  67. package/bridge-recipes/unreal/character-customizer/bottom.addon.json +1 -0
  68. package/bridge-recipes/unreal/character-customizer/bottom.json +6 -0
  69. package/bridge-recipes/unreal/character-customizer/eyewear.addon.json +1 -0
  70. package/bridge-recipes/unreal/character-customizer/eyewear.json +6 -0
  71. package/bridge-recipes/unreal/character-customizer/footwear.addon.json +1 -0
  72. package/bridge-recipes/unreal/character-customizer/footwear.json +6 -0
  73. package/bridge-recipes/unreal/character-customizer/hair.addon.json +1 -0
  74. package/bridge-recipes/unreal/character-customizer/hair.json +6 -0
  75. package/bridge-recipes/unreal/character-customizer/test/recipes.test.js +74 -0
  76. package/bridge-recipes/unreal/character-customizer/top.addon.json +1 -0
  77. package/bridge-recipes/unreal/character-customizer/top.json +6 -0
  78. package/bridge-recipes/unreal/helix-character-creator-female/README.md +57 -0
  79. package/bridge-recipes/unreal/helix-character-creator-female/all.json +18 -0
  80. package/bridge-recipes/unreal/helix-character-creator-female/back.addon.json +1 -0
  81. package/bridge-recipes/unreal/helix-character-creator-female/back.json +1 -0
  82. package/bridge-recipes/unreal/helix-character-creator-female/base-female-face.json +1 -0
  83. package/bridge-recipes/unreal/helix-character-creator-female/base-female.json +1 -0
  84. package/bridge-recipes/unreal/helix-character-creator-female/bottom.addon.json +1 -0
  85. package/bridge-recipes/unreal/helix-character-creator-female/bottom.json +1 -0
  86. package/bridge-recipes/unreal/helix-character-creator-female/eyewear.addon.json +1 -0
  87. package/bridge-recipes/unreal/helix-character-creator-female/eyewear.json +1 -0
  88. package/bridge-recipes/unreal/helix-character-creator-female/female-basic-t-shirt.addon.json +1 -0
  89. package/bridge-recipes/unreal/helix-character-creator-female/female-basic-t-shirt.json +1 -0
  90. package/bridge-recipes/unreal/helix-character-creator-female/female-batch-hair.json +23 -0
  91. package/bridge-recipes/unreal/helix-character-creator-female/female-batch-nebula-urbanghost.json +7 -0
  92. package/bridge-recipes/unreal/helix-character-creator-female/female-batch-singles.json +19 -0
  93. package/bridge-recipes/unreal/helix-character-creator-female/female-batch5.json +19 -0
  94. package/bridge-recipes/unreal/helix-character-creator-female/female-batch6.json +19 -0
  95. package/bridge-recipes/unreal/helix-character-creator-female/female-batch7.json +11 -0
  96. package/bridge-recipes/unreal/helix-character-creator-female/female-cap-2.addon.json +1 -0
  97. package/bridge-recipes/unreal/helix-character-creator-female/female-cap-2.json +1 -0
  98. package/bridge-recipes/unreal/helix-character-creator-female/female-cap.addon.json +1 -0
  99. package/bridge-recipes/unreal/helix-character-creator-female/female-cap.json +1 -0
  100. package/bridge-recipes/unreal/helix-character-creator-female/female-cargo-pants.addon.json +1 -0
  101. package/bridge-recipes/unreal/helix-character-creator-female/female-cargo-pants.json +1 -0
  102. package/bridge-recipes/unreal/helix-character-creator-female/female-carnival-mask-3.addon.json +1 -0
  103. package/bridge-recipes/unreal/helix-character-creator-female/female-carnival-mask-3.json +1 -0
  104. package/bridge-recipes/unreal/helix-character-creator-female/female-chain.addon.json +1 -0
  105. package/bridge-recipes/unreal/helix-character-creator-female/female-chain.json +1 -0
  106. package/bridge-recipes/unreal/helix-character-creator-female/female-clare-pants.addon.json +1 -0
  107. package/bridge-recipes/unreal/helix-character-creator-female/female-clare-pants.json +1 -0
  108. package/bridge-recipes/unreal/helix-character-creator-female/female-clare-shoes.addon.json +1 -0
  109. package/bridge-recipes/unreal/helix-character-creator-female/female-clare-shoes.json +1 -0
  110. package/bridge-recipes/unreal/helix-character-creator-female/female-clare-top.addon.json +1 -0
  111. package/bridge-recipes/unreal/helix-character-creator-female/female-clare-top.json +1 -0
  112. package/bridge-recipes/unreal/helix-character-creator-female/female-compression-bodysuit-2.addon.json +1 -0
  113. package/bridge-recipes/unreal/helix-character-creator-female/female-compression-bodysuit-2.json +1 -0
  114. package/bridge-recipes/unreal/helix-character-creator-female/female-compression-bodysuit.addon.json +1 -0
  115. package/bridge-recipes/unreal/helix-character-creator-female/female-compression-bodysuit.json +1 -0
  116. package/bridge-recipes/unreal/helix-character-creator-female/female-eye-hole-mask-2.addon.json +1 -0
  117. package/bridge-recipes/unreal/helix-character-creator-female/female-eye-hole-mask-2.json +1 -0
  118. package/bridge-recipes/unreal/helix-character-creator-female/female-eye-patch.addon.json +1 -0
  119. package/bridge-recipes/unreal/helix-character-creator-female/female-eye-patch.json +1 -0
  120. package/bridge-recipes/unreal/helix-character-creator-female/female-firefight-outfit.addon.json +1 -0
  121. package/bridge-recipes/unreal/helix-character-creator-female/female-firefight-outfit.json +1 -0
  122. package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-pants.addon.json +1 -0
  123. package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-pants.json +1 -0
  124. package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-sneakers.addon.json +1 -0
  125. package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-sneakers.json +1 -0
  126. package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-top.addon.json +1 -0
  127. package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-top.json +1 -0
  128. package/bridge-recipes/unreal/helix-character-creator-female/female-gamer-mask.addon.json +1 -0
  129. package/bridge-recipes/unreal/helix-character-creator-female/female-gamer-mask.json +1 -0
  130. package/bridge-recipes/unreal/helix-character-creator-female/female-guy-fawkes-mask.addon.json +1 -0
  131. package/bridge-recipes/unreal/helix-character-creator-female/female-guy-fawkes-mask.json +1 -0
  132. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a1.addon.json +1 -0
  133. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a1.json +1 -0
  134. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a10.addon.json +1 -0
  135. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a10.json +1 -0
  136. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a11.addon.json +1 -0
  137. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a11.json +1 -0
  138. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a12.addon.json +1 -0
  139. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a12.json +1 -0
  140. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a13.addon.json +1 -0
  141. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a13.json +1 -0
  142. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a14.addon.json +1 -0
  143. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a14.json +1 -0
  144. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a15.addon.json +1 -0
  145. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a15.json +1 -0
  146. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a16.addon.json +1 -0
  147. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a16.json +1 -0
  148. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a17.addon.json +1 -0
  149. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a17.json +1 -0
  150. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a18.addon.json +1 -0
  151. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a18.json +1 -0
  152. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a19.addon.json +1 -0
  153. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a19.json +1 -0
  154. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a2.addon.json +1 -0
  155. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a2.json +1 -0
  156. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a20.addon.json +1 -0
  157. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a20.json +1 -0
  158. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a21.addon.json +1 -0
  159. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a21.json +1 -0
  160. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a3.addon.json +1 -0
  161. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a3.json +1 -0
  162. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a4.addon.json +1 -0
  163. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a4.json +1 -0
  164. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a5.addon.json +1 -0
  165. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a5.json +1 -0
  166. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a6.addon.json +1 -0
  167. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a6.json +1 -0
  168. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a7.addon.json +1 -0
  169. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a7.json +1 -0
  170. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a8.addon.json +1 -0
  171. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a8.json +1 -0
  172. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a9.addon.json +1 -0
  173. package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a9.json +1 -0
  174. package/bridge-recipes/unreal/helix-character-creator-female/female-high-waist-tailored-trousers.addon.json +1 -0
  175. package/bridge-recipes/unreal/helix-character-creator-female/female-high-waist-tailored-trousers.json +1 -0
  176. package/bridge-recipes/unreal/helix-character-creator-female/female-hogwarts-pants.addon.json +1 -0
  177. package/bridge-recipes/unreal/helix-character-creator-female/female-hogwarts-pants.json +1 -0
  178. package/bridge-recipes/unreal/helix-character-creator-female/female-hoody.addon.json +1 -0
  179. package/bridge-recipes/unreal/helix-character-creator-female/female-hoody.json +1 -0
  180. package/bridge-recipes/unreal/helix-character-creator-female/female-jacket-6.addon.json +1 -0
  181. package/bridge-recipes/unreal/helix-character-creator-female/female-jacket-6.json +1 -0
  182. package/bridge-recipes/unreal/helix-character-creator-female/female-joker-mask.addon.json +1 -0
  183. package/bridge-recipes/unreal/helix-character-creator-female/female-joker-mask.json +1 -0
  184. package/bridge-recipes/unreal/helix-character-creator-female/female-kombes-brown.addon.json +1 -0
  185. package/bridge-recipes/unreal/helix-character-creator-female/female-kombes-brown.json +1 -0
  186. package/bridge-recipes/unreal/helix-character-creator-female/female-kombes.addon.json +1 -0
  187. package/bridge-recipes/unreal/helix-character-creator-female/female-kombes.json +1 -0
  188. package/bridge-recipes/unreal/helix-character-creator-female/female-marshmallow-mask.addon.json +1 -0
  189. package/bridge-recipes/unreal/helix-character-creator-female/female-marshmallow-mask.json +1 -0
  190. package/bridge-recipes/unreal/helix-character-creator-female/female-maska.addon.json +1 -0
  191. package/bridge-recipes/unreal/helix-character-creator-female/female-maska.json +1 -0
  192. package/bridge-recipes/unreal/helix-character-creator-female/female-nebula-executive-pants.addon.json +1 -0
  193. package/bridge-recipes/unreal/helix-character-creator-female/female-nebula-executive-pants.json +1 -0
  194. package/bridge-recipes/unreal/helix-character-creator-female/female-nebula-executive-shoes.addon.json +1 -0
  195. package/bridge-recipes/unreal/helix-character-creator-female/female-nebula-executive-shoes.json +1 -0
  196. package/bridge-recipes/unreal/helix-character-creator-female/female-ninza-costume.addon.json +1 -0
  197. package/bridge-recipes/unreal/helix-character-creator-female/female-ninza-costume.json +1 -0
  198. package/bridge-recipes/unreal/helix-character-creator-female/female-orange-kombes.addon.json +1 -0
  199. package/bridge-recipes/unreal/helix-character-creator-female/female-orange-kombes.json +1 -0
  200. package/bridge-recipes/unreal/helix-character-creator-female/female-panama.addon.json +1 -0
  201. package/bridge-recipes/unreal/helix-character-creator-female/female-panama.json +1 -0
  202. package/bridge-recipes/unreal/helix-character-creator-female/female-panda-bag-2.addon.json +1 -0
  203. package/bridge-recipes/unreal/helix-character-creator-female/female-panda-bag-2.json +1 -0
  204. package/bridge-recipes/unreal/helix-character-creator-female/female-pantslow.addon.json +1 -0
  205. package/bridge-recipes/unreal/helix-character-creator-female/female-pantslow.json +1 -0
  206. package/bridge-recipes/unreal/helix-character-creator-female/female-paper-bag-mask.addon.json +1 -0
  207. package/bridge-recipes/unreal/helix-character-creator-female/female-paper-bag-mask.json +1 -0
  208. package/bridge-recipes/unreal/helix-character-creator-female/female-pig-mask.addon.json +1 -0
  209. package/bridge-recipes/unreal/helix-character-creator-female/female-pig-mask.json +1 -0
  210. package/bridge-recipes/unreal/helix-character-creator-female/female-police-uniform.addon.json +1 -0
  211. package/bridge-recipes/unreal/helix-character-creator-female/female-police-uniform.json +1 -0
  212. package/bridge-recipes/unreal/helix-character-creator-female/female-prison-outfit.addon.json +1 -0
  213. package/bridge-recipes/unreal/helix-character-creator-female/female-prison-outfit.json +1 -0
  214. package/bridge-recipes/unreal/helix-character-creator-female/female-short-b4low.addon.json +1 -0
  215. package/bridge-recipes/unreal/helix-character-creator-female/female-short-b4low.json +1 -0
  216. package/bridge-recipes/unreal/helix-character-creator-female/female-short-swimming.addon.json +1 -0
  217. package/bridge-recipes/unreal/helix-character-creator-female/female-short-swimming.json +1 -0
  218. package/bridge-recipes/unreal/helix-character-creator-female/female-shortlow.addon.json +1 -0
  219. package/bridge-recipes/unreal/helix-character-creator-female/female-shortlow.json +1 -0
  220. package/bridge-recipes/unreal/helix-character-creator-female/female-shorts-b3.addon.json +1 -0
  221. package/bridge-recipes/unreal/helix-character-creator-female/female-shorts-b3.json +1 -0
  222. package/bridge-recipes/unreal/helix-character-creator-female/female-sterile-gloves.addon.json +1 -0
  223. package/bridge-recipes/unreal/helix-character-creator-female/female-sterile-gloves.json +1 -0
  224. package/bridge-recipes/unreal/helix-character-creator-female/female-surgeon-mask.addon.json +1 -0
  225. package/bridge-recipes/unreal/helix-character-creator-female/female-surgeon-mask.json +1 -0
  226. package/bridge-recipes/unreal/helix-character-creator-female/female-swat.addon.json +1 -0
  227. package/bridge-recipes/unreal/helix-character-creator-female/female-swat.json +1 -0
  228. package/bridge-recipes/unreal/helix-character-creator-female/female-top.addon.json +1 -0
  229. package/bridge-recipes/unreal/helix-character-creator-female/female-top.json +1 -0
  230. package/bridge-recipes/unreal/helix-character-creator-female/female-topx1.addon.json +1 -0
  231. package/bridge-recipes/unreal/helix-character-creator-female/female-topx1.json +1 -0
  232. package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-hoodie.addon.json +1 -0
  233. package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-hoodie.json +1 -0
  234. package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-mask.addon.json +1 -0
  235. package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-mask.json +1 -0
  236. package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-pants.addon.json +1 -0
  237. package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-pants.json +1 -0
  238. package/bridge-recipes/unreal/helix-character-creator-female/female-warehouse-uniform.addon.json +1 -0
  239. package/bridge-recipes/unreal/helix-character-creator-female/female-warehouse-uniform.json +1 -0
  240. package/bridge-recipes/unreal/helix-character-creator-female/female-wide-top.addon.json +1 -0
  241. package/bridge-recipes/unreal/helix-character-creator-female/female-wide-top.json +1 -0
  242. package/bridge-recipes/unreal/helix-character-creator-female/female-wolf-set.addon.json +1 -0
  243. package/bridge-recipes/unreal/helix-character-creator-female/female-wolf-set.json +1 -0
  244. package/bridge-recipes/unreal/helix-character-creator-female/female-woman-mask-3.addon.json +1 -0
  245. package/bridge-recipes/unreal/helix-character-creator-female/female-woman-mask-3.json +1 -0
  246. package/bridge-recipes/unreal/helix-character-creator-female/female-women-out-2.addon.json +1 -0
  247. package/bridge-recipes/unreal/helix-character-creator-female/female-women-out-2.json +1 -0
  248. package/bridge-recipes/unreal/helix-character-creator-female/footwear.addon.json +1 -0
  249. package/bridge-recipes/unreal/helix-character-creator-female/footwear.json +1 -0
  250. package/bridge-recipes/unreal/helix-character-creator-female/gloves.addon.json +1 -0
  251. package/bridge-recipes/unreal/helix-character-creator-female/gloves.json +1 -0
  252. package/bridge-recipes/unreal/helix-character-creator-female/hair.addon.json +1 -0
  253. package/bridge-recipes/unreal/helix-character-creator-female/hair.json +1 -0
  254. package/bridge-recipes/unreal/helix-character-creator-female/headwear.addon.json +1 -0
  255. package/bridge-recipes/unreal/helix-character-creator-female/headwear.json +1 -0
  256. package/bridge-recipes/unreal/helix-character-creator-female/mask.addon.json +1 -0
  257. package/bridge-recipes/unreal/helix-character-creator-female/mask.json +1 -0
  258. package/bridge-recipes/unreal/helix-character-creator-female/neck.addon.json +1 -0
  259. package/bridge-recipes/unreal/helix-character-creator-female/neck.json +1 -0
  260. package/bridge-recipes/unreal/helix-character-creator-female/outfit.addon.json +1 -0
  261. package/bridge-recipes/unreal/helix-character-creator-female/outfit.json +1 -0
  262. package/bridge-recipes/unreal/helix-character-creator-female/top.addon.json +1 -0
  263. package/bridge-recipes/unreal/helix-character-creator-female/top.json +1 -0
  264. package/bridge-recipes/unreal/helix-character-creator-male/README.md +15 -0
  265. package/bridge-recipes/unreal/helix-character-creator-male/all.json +18 -0
  266. package/bridge-recipes/unreal/helix-character-creator-male/back.addon.json +1 -0
  267. package/bridge-recipes/unreal/helix-character-creator-male/back.json +1 -0
  268. package/bridge-recipes/unreal/helix-character-creator-male/base-male-face.json +6 -0
  269. package/bridge-recipes/unreal/helix-character-creator-male/base-male.json +6 -0
  270. package/bridge-recipes/unreal/helix-character-creator-male/bottom.addon.json +1 -0
  271. package/bridge-recipes/unreal/helix-character-creator-male/bottom.json +1 -0
  272. package/bridge-recipes/unreal/helix-character-creator-male/eyewear.addon.json +1 -0
  273. package/bridge-recipes/unreal/helix-character-creator-male/eyewear.json +1 -0
  274. package/bridge-recipes/unreal/helix-character-creator-male/footwear.addon.json +1 -0
  275. package/bridge-recipes/unreal/helix-character-creator-male/footwear.json +1 -0
  276. package/bridge-recipes/unreal/helix-character-creator-male/gloves.addon.json +1 -0
  277. package/bridge-recipes/unreal/helix-character-creator-male/gloves.json +1 -0
  278. package/bridge-recipes/unreal/helix-character-creator-male/hair.addon.json +1 -0
  279. package/bridge-recipes/unreal/helix-character-creator-male/hair.json +1 -0
  280. package/bridge-recipes/unreal/helix-character-creator-male/headwear.addon.json +1 -0
  281. package/bridge-recipes/unreal/helix-character-creator-male/headwear.json +1 -0
  282. package/bridge-recipes/unreal/helix-character-creator-male/male-batch-hair.json +23 -0
  283. package/bridge-recipes/unreal/helix-character-creator-male/male-batch-nebula-urbanghost.json +7 -0
  284. package/bridge-recipes/unreal/helix-character-creator-male/male-batch-uniforms.json +10 -0
  285. package/bridge-recipes/unreal/helix-character-creator-male/male-batch6-slot2.json +7 -0
  286. package/bridge-recipes/unreal/helix-character-creator-male/male-batch7-slot3.json +6 -0
  287. package/bridge-recipes/unreal/helix-character-creator-male/male-batch8-combined.json +11 -0
  288. package/bridge-recipes/unreal/helix-character-creator-male/male-boots-25.addon.json +1 -0
  289. package/bridge-recipes/unreal/helix-character-creator-male/male-cargo-pants.addon.json +1 -0
  290. package/bridge-recipes/unreal/helix-character-creator-male/male-cargo-pants.json +1 -0
  291. package/bridge-recipes/unreal/helix-character-creator-male/male-carnival-mask-2.addon.json +1 -0
  292. package/bridge-recipes/unreal/helix-character-creator-male/male-carnival-mask-2.json +1 -0
  293. package/bridge-recipes/unreal/helix-character-creator-male/male-carnival-mask-3.addon.json +1 -0
  294. package/bridge-recipes/unreal/helix-character-creator-male/male-carnival-mask-3.json +1 -0
  295. package/bridge-recipes/unreal/helix-character-creator-male/male-clare-pants.addon.json +1 -0
  296. package/bridge-recipes/unreal/helix-character-creator-male/male-clare-pants.json +1 -0
  297. package/bridge-recipes/unreal/helix-character-creator-male/male-clare-shoes.addon.json +1 -0
  298. package/bridge-recipes/unreal/helix-character-creator-male/male-clare-shoes.json +1 -0
  299. package/bridge-recipes/unreal/helix-character-creator-male/male-clare-top.addon.json +1 -0
  300. package/bridge-recipes/unreal/helix-character-creator-male/male-clare-top.json +1 -0
  301. package/bridge-recipes/unreal/helix-character-creator-male/male-diamond-hands.addon.json +1 -0
  302. package/bridge-recipes/unreal/helix-character-creator-male/male-diamond-hands.json +1 -0
  303. package/bridge-recipes/unreal/helix-character-creator-male/male-doctor-outfit.addon.json +1 -0
  304. package/bridge-recipes/unreal/helix-character-creator-male/male-doctor-outfit.json +1 -0
  305. package/bridge-recipes/unreal/helix-character-creator-male/male-eye-hole-mask-2.addon.json +1 -0
  306. package/bridge-recipes/unreal/helix-character-creator-male/male-eye-hole-mask-2.json +1 -0
  307. package/bridge-recipes/unreal/helix-character-creator-male/male-eye-hole-mask.addon.json +1 -0
  308. package/bridge-recipes/unreal/helix-character-creator-male/male-eye-hole-mask.json +1 -0
  309. package/bridge-recipes/unreal/helix-character-creator-male/male-eye-patch.addon.json +1 -0
  310. package/bridge-recipes/unreal/helix-character-creator-male/male-eye-patch.json +1 -0
  311. package/bridge-recipes/unreal/helix-character-creator-male/male-fb-cargo-pants-2.addon.json +1 -0
  312. package/bridge-recipes/unreal/helix-character-creator-male/male-fb-cargo-pants-2.json +1 -0
  313. package/bridge-recipes/unreal/helix-character-creator-male/male-fb-cargo-pants.addon.json +1 -0
  314. package/bridge-recipes/unreal/helix-character-creator-male/male-fb-puffy.addon.json +1 -0
  315. package/bridge-recipes/unreal/helix-character-creator-male/male-fb-puffy.json +1 -0
  316. package/bridge-recipes/unreal/helix-character-creator-male/male-firefight-outfit.addon.json +1 -0
  317. package/bridge-recipes/unreal/helix-character-creator-male/male-firefight-outfit.json +1 -0
  318. package/bridge-recipes/unreal/helix-character-creator-male/male-gamer-mask-2.addon.json +1 -0
  319. package/bridge-recipes/unreal/helix-character-creator-male/male-gamer-mask-2.json +1 -0
  320. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a1.addon.json +1 -0
  321. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a1.json +1 -0
  322. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a10.addon.json +1 -0
  323. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a10.json +1 -0
  324. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a11.addon.json +1 -0
  325. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a11.json +1 -0
  326. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a12.addon.json +1 -0
  327. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a12.json +1 -0
  328. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a13.addon.json +1 -0
  329. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a13.json +1 -0
  330. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a14.addon.json +1 -0
  331. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a14.json +1 -0
  332. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a15.addon.json +1 -0
  333. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a15.json +1 -0
  334. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a16.addon.json +1 -0
  335. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a16.json +1 -0
  336. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a17.addon.json +1 -0
  337. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a17.json +1 -0
  338. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a18.addon.json +1 -0
  339. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a18.json +1 -0
  340. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a19.addon.json +1 -0
  341. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a19.json +1 -0
  342. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a2.addon.json +1 -0
  343. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a2.json +1 -0
  344. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a20.addon.json +1 -0
  345. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a20.json +1 -0
  346. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a21.addon.json +1 -0
  347. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a21.json +1 -0
  348. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a3.addon.json +1 -0
  349. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a3.json +1 -0
  350. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a4.addon.json +1 -0
  351. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a4.json +1 -0
  352. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a5.addon.json +1 -0
  353. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a5.json +1 -0
  354. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a6.addon.json +1 -0
  355. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a6.json +1 -0
  356. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a7.addon.json +1 -0
  357. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a7.json +1 -0
  358. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a8.addon.json +1 -0
  359. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a8.json +1 -0
  360. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a9.addon.json +1 -0
  361. package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a9.json +1 -0
  362. package/bridge-recipes/unreal/helix-character-creator-male/male-kombes-brown-v2.addon.json +1 -0
  363. package/bridge-recipes/unreal/helix-character-creator-male/male-kombes-brown-v2.json +1 -0
  364. package/bridge-recipes/unreal/helix-character-creator-male/male-kombes-brown.addon.json +1 -0
  365. package/bridge-recipes/unreal/helix-character-creator-male/male-man-mask-3.addon.json +1 -0
  366. package/bridge-recipes/unreal/helix-character-creator-male/male-marshmallow-mask.addon.json +1 -0
  367. package/bridge-recipes/unreal/helix-character-creator-male/male-marshmallow-mask.json +1 -0
  368. package/bridge-recipes/unreal/helix-character-creator-male/male-mikejack-jacket.addon.json +1 -0
  369. package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-coat.addon.json +1 -0
  370. package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-coat.json +1 -0
  371. package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-pants.addon.json +1 -0
  372. package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-pants.json +1 -0
  373. package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-shoes.addon.json +1 -0
  374. package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-shoes.json +1 -0
  375. package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-gloves.addon.json +1 -0
  376. package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-gloves.json +1 -0
  377. package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-mantle.addon.json +1 -0
  378. package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-mantle.json +1 -0
  379. package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-tunic.addon.json +1 -0
  380. package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-tunic.json +1 -0
  381. package/bridge-recipes/unreal/helix-character-creator-male/male-orangekombes.addon.json +1 -0
  382. package/bridge-recipes/unreal/helix-character-creator-male/male-orangekombes.json +1 -0
  383. package/bridge-recipes/unreal/helix-character-creator-male/male-out2-boots.addon.json +1 -0
  384. package/bridge-recipes/unreal/helix-character-creator-male/male-out2-boots.json +1 -0
  385. package/bridge-recipes/unreal/helix-character-creator-male/male-panda-bag-2.addon.json +1 -0
  386. package/bridge-recipes/unreal/helix-character-creator-male/male-panda-bag-2.json +1 -0
  387. package/bridge-recipes/unreal/helix-character-creator-male/male-pants-2.addon.json +1 -0
  388. package/bridge-recipes/unreal/helix-character-creator-male/male-pants-2.json +1 -0
  389. package/bridge-recipes/unreal/helix-character-creator-male/male-paper-bag-mask.addon.json +1 -0
  390. package/bridge-recipes/unreal/helix-character-creator-male/male-paper-bag-mask.json +1 -0
  391. package/bridge-recipes/unreal/helix-character-creator-male/male-pig-mask.addon.json +1 -0
  392. package/bridge-recipes/unreal/helix-character-creator-male/male-pig-mask.json +1 -0
  393. package/bridge-recipes/unreal/helix-character-creator-male/male-police-uniform.addon.json +1 -0
  394. package/bridge-recipes/unreal/helix-character-creator-male/male-police-uniform.json +1 -0
  395. package/bridge-recipes/unreal/helix-character-creator-male/male-prison-outfit.addon.json +1 -0
  396. package/bridge-recipes/unreal/helix-character-creator-male/male-prison-outfit.json +1 -0
  397. package/bridge-recipes/unreal/helix-character-creator-male/male-puffer-jacket.addon.json +1 -0
  398. package/bridge-recipes/unreal/helix-character-creator-male/male-puffer-jacket.json +1 -0
  399. package/bridge-recipes/unreal/helix-character-creator-male/male-pullover-hoodie.addon.json +1 -0
  400. package/bridge-recipes/unreal/helix-character-creator-male/male-pullover-hoodie.json +1 -0
  401. package/bridge-recipes/unreal/helix-character-creator-male/male-robber-mask.addon.json +1 -0
  402. package/bridge-recipes/unreal/helix-character-creator-male/male-robber-mask.json +1 -0
  403. package/bridge-recipes/unreal/helix-character-creator-male/male-short-swimming.addon.json +1 -0
  404. package/bridge-recipes/unreal/helix-character-creator-male/male-short-swimming.json +1 -0
  405. package/bridge-recipes/unreal/helix-character-creator-male/male-sinth-ascetic-jacket.addon.json +1 -0
  406. package/bridge-recipes/unreal/helix-character-creator-male/male-sinth-ascetic-jacket.json +1 -0
  407. package/bridge-recipes/unreal/helix-character-creator-male/male-sip-cap.addon.json +1 -0
  408. package/bridge-recipes/unreal/helix-character-creator-male/male-sip-cap.json +1 -0
  409. package/bridge-recipes/unreal/helix-character-creator-male/male-socks.addon.json +1 -0
  410. package/bridge-recipes/unreal/helix-character-creator-male/male-socks.json +1 -0
  411. package/bridge-recipes/unreal/helix-character-creator-male/male-surgeon-mask.addon.json +1 -0
  412. package/bridge-recipes/unreal/helix-character-creator-male/male-surgeon-mask.json +1 -0
  413. package/bridge-recipes/unreal/helix-character-creator-male/male-tech-hoodie.addon.json +1 -0
  414. package/bridge-recipes/unreal/helix-character-creator-male/male-tech-hoodie.json +1 -0
  415. package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-hoodie.addon.json +1 -0
  416. package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-hoodie.json +1 -0
  417. package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-mask.addon.json +1 -0
  418. package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-mask.json +1 -0
  419. package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-pants.addon.json +1 -0
  420. package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-pants.json +1 -0
  421. package/bridge-recipes/unreal/helix-character-creator-male/male-warehouse-uniform.addon.json +1 -0
  422. package/bridge-recipes/unreal/helix-character-creator-male/male-warehouse-uniform.json +1 -0
  423. package/bridge-recipes/unreal/helix-character-creator-male/mask.addon.json +1 -0
  424. package/bridge-recipes/unreal/helix-character-creator-male/mask.json +1 -0
  425. package/bridge-recipes/unreal/helix-character-creator-male/neck.addon.json +1 -0
  426. package/bridge-recipes/unreal/helix-character-creator-male/neck.json +1 -0
  427. package/bridge-recipes/unreal/helix-character-creator-male/outfit.addon.json +1 -0
  428. package/bridge-recipes/unreal/helix-character-creator-male/outfit.json +1 -0
  429. package/bridge-recipes/unreal/helix-character-creator-male/palette-pilot.json +1 -0
  430. package/bridge-recipes/unreal/helix-character-creator-male/test/recipes.test.js +122 -0
  431. package/bridge-recipes/unreal/helix-character-creator-male/top.addon.json +1 -0
  432. package/bridge-recipes/unreal/helix-character-creator-male/top.json +1 -0
  433. package/bridge-recipes/unreal/helix-character-creator-male/variants/bottom-nopalette.addon.json +1 -0
  434. package/bridge-recipes/unreal/helix-character-creator-male/variants/headwear-nopalette.addon.json +1 -0
  435. package/bridge-recipes/unreal/helix-character-creator-male/variants/neck-nopalette.addon.json +1 -0
  436. package/dist/ability.d.ts +2 -2
  437. package/dist/ability.js +6 -5
  438. package/dist/ability.js.map +1 -1
  439. package/dist/achievement.d.ts +123 -0
  440. package/dist/achievement.js +400 -0
  441. package/dist/achievement.js.map +1 -0
  442. package/dist/api.d.ts +19 -1
  443. package/dist/api.js +80 -6
  444. package/dist/api.js.map +1 -1
  445. package/dist/assets.d.ts +595 -0
  446. package/dist/assets.js +1121 -0
  447. package/dist/assets.js.map +1 -0
  448. package/dist/audioCommand.d.ts +65 -0
  449. package/dist/audioCommand.js +181 -0
  450. package/dist/audioCommand.js.map +1 -0
  451. package/dist/bakeBodyRegionsCli.d.ts +19 -0
  452. package/dist/bakeBodyRegionsCli.js +25 -0
  453. package/dist/bakeBodyRegionsCli.js.map +1 -0
  454. package/dist/bakeLighting.d.ts +173 -0
  455. package/dist/bakeLighting.js +452 -0
  456. package/dist/bakeLighting.js.map +1 -0
  457. package/dist/bridge/bridge.testutil.d.ts +105 -0
  458. package/dist/bridge/bridge.testutil.js +259 -0
  459. package/dist/bridge/bridge.testutil.js.map +1 -0
  460. package/dist/bridge/characterAddon.d.ts +47 -0
  461. package/dist/bridge/characterAddon.js +146 -0
  462. package/dist/bridge/characterAddon.js.map +1 -0
  463. package/dist/bridge/detect.d.ts +4 -0
  464. package/dist/bridge/detect.js +154 -0
  465. package/dist/bridge/detect.js.map +1 -0
  466. package/dist/bridge/execute.d.ts +3 -0
  467. package/dist/bridge/execute.js +206 -0
  468. package/dist/bridge/execute.js.map +1 -0
  469. package/dist/bridge/glb.d.ts +60 -0
  470. package/dist/bridge/glb.js +81 -0
  471. package/dist/bridge/glb.js.map +1 -0
  472. package/dist/bridge/hash.d.ts +6 -0
  473. package/dist/bridge/hash.js +40 -0
  474. package/dist/bridge/hash.js.map +1 -0
  475. package/dist/bridge/index.d.ts +11 -0
  476. package/dist/bridge/index.js +28 -0
  477. package/dist/bridge/index.js.map +1 -0
  478. package/dist/bridge/internal/artifacts.d.ts +5 -0
  479. package/dist/bridge/internal/artifacts.js +31 -0
  480. package/dist/bridge/internal/artifacts.js.map +1 -0
  481. package/dist/bridge/internal/builtInExecution.d.ts +4 -0
  482. package/dist/bridge/internal/builtInExecution.js +514 -0
  483. package/dist/bridge/internal/builtInExecution.js.map +1 -0
  484. package/dist/bridge/internal/externalExecution.d.ts +12 -0
  485. package/dist/bridge/internal/externalExecution.js +261 -0
  486. package/dist/bridge/internal/externalExecution.js.map +1 -0
  487. package/dist/bridge/internal/outputReservation.d.ts +13 -0
  488. package/dist/bridge/internal/outputReservation.js +151 -0
  489. package/dist/bridge/internal/outputReservation.js.map +1 -0
  490. package/dist/bridge/internal/snapshots.d.ts +32 -0
  491. package/dist/bridge/internal/snapshots.js +562 -0
  492. package/dist/bridge/internal/snapshots.js.map +1 -0
  493. package/dist/bridge/management.d.ts +40 -0
  494. package/dist/bridge/management.js +333 -0
  495. package/dist/bridge/management.js.map +1 -0
  496. package/dist/bridge/manifest.d.ts +8 -0
  497. package/dist/bridge/manifest.js +192 -0
  498. package/dist/bridge/manifest.js.map +1 -0
  499. package/dist/bridge/metahuman.d.ts +26 -0
  500. package/dist/bridge/metahuman.js +332 -0
  501. package/dist/bridge/metahuman.js.map +1 -0
  502. package/dist/bridge/planner.d.ts +9 -0
  503. package/dist/bridge/planner.js +246 -0
  504. package/dist/bridge/planner.js.map +1 -0
  505. package/dist/bridge/plugins.d.ts +17 -0
  506. package/dist/bridge/plugins.js +307 -0
  507. package/dist/bridge/plugins.js.map +1 -0
  508. package/dist/bridge/resolution.d.ts +8 -0
  509. package/dist/bridge/resolution.js +55 -0
  510. package/dist/bridge/resolution.js.map +1 -0
  511. package/dist/bridge/types.d.ts +247 -0
  512. package/dist/bridge/types.js +60 -0
  513. package/dist/bridge/types.js.map +1 -0
  514. package/dist/bridge/validate.d.ts +12 -0
  515. package/dist/bridge/validate.js +370 -0
  516. package/dist/bridge/validate.js.map +1 -0
  517. package/dist/bridge/zipSource.d.ts +27 -0
  518. package/dist/bridge/zipSource.js +90 -0
  519. package/dist/bridge/zipSource.js.map +1 -0
  520. package/dist/browser/hostSlots.d.ts +57 -0
  521. package/dist/browser/hostSlots.js +371 -0
  522. package/dist/browser/hostSlots.js.map +1 -0
  523. package/dist/browser/launch.d.ts +25 -0
  524. package/dist/browser/launch.js +74 -0
  525. package/dist/browser/launch.js.map +1 -0
  526. package/dist/browser/nonSystemChrome.d.ts +15 -0
  527. package/dist/browser/nonSystemChrome.js +91 -0
  528. package/dist/browser/nonSystemChrome.js.map +1 -0
  529. package/dist/builds/index.d.ts +54 -0
  530. package/dist/builds/index.js +215 -0
  531. package/dist/builds/index.js.map +1 -0
  532. package/dist/bundle.d.ts +35 -2
  533. package/dist/bundle.js +437 -20
  534. package/dist/bundle.js.map +1 -1
  535. package/dist/canary/cli.d.ts +17 -0
  536. package/dist/canary/cli.js +95 -0
  537. package/dist/canary/cli.js.map +1 -0
  538. package/dist/canary/index.d.ts +4 -0
  539. package/dist/canary/index.js +21 -0
  540. package/dist/canary/index.js.map +1 -0
  541. package/dist/canary/inventory.d.ts +46 -0
  542. package/dist/canary/inventory.js +499 -0
  543. package/dist/canary/inventory.js.map +1 -0
  544. package/dist/canary/pacifica.d.ts +51 -0
  545. package/dist/canary/pacifica.js +247 -0
  546. package/dist/canary/pacifica.js.map +1 -0
  547. package/dist/canary/plan.d.ts +115 -0
  548. package/dist/canary/plan.js +479 -0
  549. package/dist/canary/plan.js.map +1 -0
  550. package/dist/character/addonPushOut.d.ts +66 -0
  551. package/dist/character/addonPushOut.js +211 -0
  552. package/dist/character/addonPushOut.js.map +1 -0
  553. package/dist/character/avatarPreflight.d.ts +92 -0
  554. package/dist/character/avatarPreflight.js +261 -0
  555. package/dist/character/avatarPreflight.js.map +1 -0
  556. package/dist/character/bakeBodyRegions.d.ts +80 -0
  557. package/dist/character/bakeBodyRegions.js +190 -0
  558. package/dist/character/bakeBodyRegions.js.map +1 -0
  559. package/dist/character/baseColorGuard.d.ts +62 -0
  560. package/dist/character/baseColorGuard.js +141 -0
  561. package/dist/character/baseColorGuard.js.map +1 -0
  562. package/dist/character/bodyGeometryGuard.d.ts +243 -0
  563. package/dist/character/bodyGeometryGuard.js +417 -0
  564. package/dist/character/bodyGeometryGuard.js.map +1 -0
  565. package/dist/character/bodyRegionHide.d.ts +57 -0
  566. package/dist/character/bodyRegionHide.js +97 -0
  567. package/dist/character/bodyRegionHide.js.map +1 -0
  568. package/dist/character/composeBaseMale.d.ts +25 -0
  569. package/dist/character/composeBaseMale.js +712 -0
  570. package/dist/character/composeBaseMale.js.map +1 -0
  571. package/dist/character/esm.js +3 -1
  572. package/dist/character/esm.js.map +1 -1
  573. package/dist/character/faceManifest.d.ts +53 -0
  574. package/dist/character/faceManifest.js +256 -0
  575. package/dist/character/faceManifest.js.map +1 -0
  576. package/dist/character/fbx.d.ts +61 -0
  577. package/dist/character/fbx.js +360 -0
  578. package/dist/character/fbx.js.map +1 -0
  579. package/dist/character/fitGarmentFile.d.ts +39 -0
  580. package/dist/character/fitGarmentFile.js +53 -0
  581. package/dist/character/fitGarmentFile.js.map +1 -0
  582. package/dist/character/helixPalette.d.ts +68 -0
  583. package/dist/character/helixPalette.js +183 -0
  584. package/dist/character/helixPalette.js.map +1 -0
  585. package/dist/character/hideCoverage.d.ts +135 -0
  586. package/dist/character/hideCoverage.js +450 -0
  587. package/dist/character/hideCoverage.js.map +1 -0
  588. package/dist/character/imageLimits.d.ts +3 -0
  589. package/dist/character/imageLimits.js +7 -0
  590. package/dist/character/imageLimits.js.map +1 -0
  591. package/dist/character/importCharacter.d.ts +188 -10
  592. package/dist/character/importCharacter.js +853 -76
  593. package/dist/character/importCharacter.js.map +1 -1
  594. package/dist/character/importTrust.d.ts +11 -0
  595. package/dist/character/importTrust.js +56 -0
  596. package/dist/character/importTrust.js.map +1 -0
  597. package/dist/character/ktx2Decode.d.ts +11 -0
  598. package/dist/character/ktx2Decode.js +87 -0
  599. package/dist/character/ktx2Decode.js.map +1 -0
  600. package/dist/character/mat4.d.ts +7 -0
  601. package/dist/character/mat4.js +25 -2
  602. package/dist/character/mat4.js.map +1 -1
  603. package/dist/character/meshyToHelixMap.d.ts +10 -0
  604. package/dist/character/meshyToHelixMap.js +92 -3
  605. package/dist/character/meshyToHelixMap.js.map +1 -1
  606. package/dist/character/metahumanToHelixMap.d.ts +2 -0
  607. package/dist/character/metahumanToHelixMap.js +34 -0
  608. package/dist/character/metahumanToHelixMap.js.map +1 -0
  609. package/dist/character/normalMapGuard.d.ts +59 -0
  610. package/dist/character/normalMapGuard.js +139 -0
  611. package/dist/character/normalMapGuard.js.map +1 -0
  612. package/dist/character/quat.js +2 -2
  613. package/dist/character/quat.js.map +1 -1
  614. package/dist/character/reframeSkeletonFile.d.ts +19 -0
  615. package/dist/character/reframeSkeletonFile.js +45 -0
  616. package/dist/character/reframeSkeletonFile.js.map +1 -0
  617. package/dist/character/repairLodSkeletonsFile.d.ts +25 -0
  618. package/dist/character/repairLodSkeletonsFile.js +49 -0
  619. package/dist/character/repairLodSkeletonsFile.js.map +1 -0
  620. package/dist/character/restFrameGuard.d.ts +52 -0
  621. package/dist/character/restFrameGuard.js +90 -0
  622. package/dist/character/restFrameGuard.js.map +1 -0
  623. package/dist/character/slotBudgets.d.ts +37 -0
  624. package/dist/character/slotBudgets.generated.d.ts +15 -0
  625. package/dist/character/slotBudgets.generated.js +110 -0
  626. package/dist/character/slotBudgets.generated.js.map +1 -0
  627. package/dist/character/slotBudgets.js +45 -0
  628. package/dist/character/slotBudgets.js.map +1 -0
  629. package/dist/character/stages/atlasMaterials.d.ts +53 -0
  630. package/dist/character/stages/atlasMaterials.js +246 -0
  631. package/dist/character/stages/atlasMaterials.js.map +1 -0
  632. package/dist/character/stages/bakeTexture.d.ts +28 -7
  633. package/dist/character/stages/bakeTexture.js +586 -108
  634. package/dist/character/stages/bakeTexture.js.map +1 -1
  635. package/dist/character/stages/conform.d.ts +54 -9
  636. package/dist/character/stages/conform.js +686 -113
  637. package/dist/character/stages/conform.js.map +1 -1
  638. package/dist/character/stages/earClearance.d.ts +105 -0
  639. package/dist/character/stages/earClearance.js +480 -0
  640. package/dist/character/stages/earClearance.js.map +1 -0
  641. package/dist/character/stages/faceRegion.d.ts +55 -0
  642. package/dist/character/stages/faceRegion.js +183 -0
  643. package/dist/character/stages/faceRegion.js.map +1 -0
  644. package/dist/character/stages/firstPersonHide.d.ts +42 -0
  645. package/dist/character/stages/firstPersonHide.js +140 -0
  646. package/dist/character/stages/firstPersonHide.js.map +1 -0
  647. package/dist/character/stages/joinPerMesh.d.ts +18 -0
  648. package/dist/character/stages/joinPerMesh.js +46 -0
  649. package/dist/character/stages/joinPerMesh.js.map +1 -0
  650. package/dist/character/stages/ktx2.d.ts +6 -4
  651. package/dist/character/stages/ktx2.js +81 -29
  652. package/dist/character/stages/ktx2.js.map +1 -1
  653. package/dist/character/stages/limbBridge.d.ts +114 -0
  654. package/dist/character/stages/limbBridge.js +1286 -0
  655. package/dist/character/stages/limbBridge.js.map +1 -0
  656. package/dist/character/stages/mergeLods.d.ts +19 -0
  657. package/dist/character/stages/mergeLods.js +58 -2
  658. package/dist/character/stages/mergeLods.js.map +1 -1
  659. package/dist/character/stages/poseAwareFit.d.ts +192 -0
  660. package/dist/character/stages/poseAwareFit.js +417 -0
  661. package/dist/character/stages/poseAwareFit.js.map +1 -0
  662. package/dist/character/stages/poseFit/math.d.ts +141 -0
  663. package/dist/character/stages/poseFit/math.js +514 -0
  664. package/dist/character/stages/poseFit/math.js.map +1 -0
  665. package/dist/character/stages/poseFit/measure.d.ts +88 -0
  666. package/dist/character/stages/poseFit/measure.js +148 -0
  667. package/dist/character/stages/poseFit/measure.js.map +1 -0
  668. package/dist/character/stages/poseFit/model.d.ts +46 -0
  669. package/dist/character/stages/poseFit/model.js +147 -0
  670. package/dist/character/stages/poseFit/model.js.map +1 -0
  671. package/dist/character/stages/poseFit/offsetField.d.ts +76 -0
  672. package/dist/character/stages/poseFit/offsetField.js +249 -0
  673. package/dist/character/stages/poseFit/offsetField.js.map +1 -0
  674. package/dist/character/stages/poseFit/posing.d.ts +52 -0
  675. package/dist/character/stages/poseFit/posing.js +330 -0
  676. package/dist/character/stages/poseFit/posing.js.map +1 -0
  677. package/dist/character/stages/poseFit/testRig.d.ts +31 -0
  678. package/dist/character/stages/poseFit/testRig.js +85 -0
  679. package/dist/character/stages/poseFit/testRig.js.map +1 -0
  680. package/dist/character/stages/reframeSkeletonToHost.d.ts +35 -0
  681. package/dist/character/stages/reframeSkeletonToHost.js +111 -0
  682. package/dist/character/stages/reframeSkeletonToHost.js.map +1 -0
  683. package/dist/character/stages/report.js +0 -2
  684. package/dist/character/stages/report.js.map +1 -1
  685. package/dist/character/stages/resizeTextures.js +4 -4
  686. package/dist/character/stages/resizeTextures.js.map +1 -1
  687. package/dist/character/stages/retarget.d.ts +8 -2
  688. package/dist/character/stages/retarget.js +52 -4
  689. package/dist/character/stages/retarget.js.map +1 -1
  690. package/dist/character/stages/retargetClip.d.ts +6 -2
  691. package/dist/character/stages/retargetClip.js +53 -16
  692. package/dist/character/stages/retargetClip.js.map +1 -1
  693. package/dist/character/stages/sanitizeMaterials.d.ts +39 -0
  694. package/dist/character/stages/sanitizeMaterials.js +136 -0
  695. package/dist/character/stages/sanitizeMaterials.js.map +1 -0
  696. package/dist/character/stages/simplifyMesh.js +6 -3
  697. package/dist/character/stages/simplifyMesh.js.map +1 -1
  698. package/dist/character/stages/splitFacePrimitive.js +0 -2
  699. package/dist/character/stages/splitFacePrimitive.js.map +1 -1
  700. package/dist/character/stages/splitSharedLodSkeletons.d.ts +31 -0
  701. package/dist/character/stages/splitSharedLodSkeletons.js +104 -0
  702. package/dist/character/stages/splitSharedLodSkeletons.js.map +1 -0
  703. package/dist/character/stages/unifyMaterial.d.ts +66 -0
  704. package/dist/character/stages/unifyMaterial.js +136 -0
  705. package/dist/character/stages/unifyMaterial.js.map +1 -0
  706. package/dist/character/stages/vrmDynamics.d.ts +144 -0
  707. package/dist/character/stages/vrmDynamics.js +421 -0
  708. package/dist/character/stages/vrmDynamics.js.map +1 -0
  709. package/dist/character/stages/vrmFace.d.ts +82 -0
  710. package/dist/character/stages/vrmFace.js +280 -0
  711. package/dist/character/stages/vrmFace.js.map +1 -0
  712. package/dist/character/stages/weldedSimplify.d.ts +15 -1
  713. package/dist/character/stages/weldedSimplify.js +90 -9
  714. package/dist/character/stages/weldedSimplify.js.map +1 -1
  715. package/dist/character/testutil/meshyBridgeFixture.d.ts +33 -0
  716. package/dist/character/testutil/meshyBridgeFixture.js +219 -0
  717. package/dist/character/testutil/meshyBridgeFixture.js.map +1 -0
  718. package/dist/character/testutil/meshyHead.d.ts +19 -0
  719. package/dist/character/testutil/meshyHead.js +99 -0
  720. package/dist/character/testutil/meshyHead.js.map +1 -0
  721. package/dist/character/testutil/meshyRig.d.ts +17 -0
  722. package/dist/character/testutil/meshyRig.js +117 -0
  723. package/dist/character/testutil/meshyRig.js.map +1 -0
  724. package/dist/character/testutil/slotBudgetExpectations.d.ts +11 -0
  725. package/dist/character/testutil/slotBudgetExpectations.js +21 -0
  726. package/dist/character/testutil/slotBudgetExpectations.js.map +1 -0
  727. package/dist/character/testutil/syntheticRig.d.ts +12 -1
  728. package/dist/character/testutil/syntheticRig.js +88 -3
  729. package/dist/character/testutil/syntheticRig.js.map +1 -1
  730. package/dist/character/unrealMaterialSidecar.d.ts +94 -0
  731. package/dist/character/unrealMaterialSidecar.js +879 -0
  732. package/dist/character/unrealMaterialSidecar.js.map +1 -0
  733. package/dist/character/untrustedSource.d.ts +54 -0
  734. package/dist/character/untrustedSource.js +181 -0
  735. package/dist/character/untrustedSource.js.map +1 -0
  736. package/dist/character/vrmCharacter.d.ts +156 -0
  737. package/dist/character/vrmCharacter.js +678 -0
  738. package/dist/character/vrmCharacter.js.map +1 -0
  739. package/dist/config.d.ts +5 -0
  740. package/dist/config.js +66 -22
  741. package/dist/config.js.map +1 -1
  742. package/dist/continuum/adminPackagePinCommand.d.ts +14 -0
  743. package/dist/continuum/adminPackagePinCommand.js +46 -0
  744. package/dist/continuum/adminPackagePinCommand.js.map +1 -0
  745. package/dist/continuum/adminPackagePublishCommand.d.ts +13 -0
  746. package/dist/continuum/adminPackagePublishCommand.js +22 -0
  747. package/dist/continuum/adminPackagePublishCommand.js.map +1 -0
  748. package/dist/continuum/authoring.d.ts +52 -0
  749. package/dist/continuum/authoring.js +143 -0
  750. package/dist/continuum/authoring.js.map +1 -0
  751. package/dist/continuum/characterMigrationGenerator.d.ts +17 -0
  752. package/dist/continuum/characterMigrationGenerator.js +242 -0
  753. package/dist/continuum/characterMigrationGenerator.js.map +1 -0
  754. package/dist/continuum/cli.d.ts +69 -0
  755. package/dist/continuum/cli.js +145 -0
  756. package/dist/continuum/cli.js.map +1 -0
  757. package/dist/continuum/cliAuthority.d.ts +17 -0
  758. package/dist/continuum/cliAuthority.js +28 -0
  759. package/dist/continuum/cliAuthority.js.map +1 -0
  760. package/dist/continuum/client.d.ts +103 -0
  761. package/dist/continuum/client.js +190 -0
  762. package/dist/continuum/client.js.map +1 -0
  763. package/dist/continuum/homeScenePackage.d.ts +159 -0
  764. package/dist/continuum/homeScenePackage.js +267 -0
  765. package/dist/continuum/homeScenePackage.js.map +1 -0
  766. package/dist/continuum/homeScenePackageCommand.d.ts +44 -0
  767. package/dist/continuum/homeScenePackageCommand.js +107 -0
  768. package/dist/continuum/homeScenePackageCommand.js.map +1 -0
  769. package/dist/continuum/identity.d.ts +4 -0
  770. package/dist/continuum/identity.js +20 -0
  771. package/dist/continuum/identity.js.map +1 -0
  772. package/dist/continuum/index.d.ts +11 -0
  773. package/dist/continuum/index.js +28 -0
  774. package/dist/continuum/index.js.map +1 -0
  775. package/dist/continuum/missingNormals.d.ts +48 -0
  776. package/dist/continuum/missingNormals.js +99 -0
  777. package/dist/continuum/missingNormals.js.map +1 -0
  778. package/dist/continuum/objectUpload.d.ts +64 -0
  779. package/dist/continuum/objectUpload.js +375 -0
  780. package/dist/continuum/objectUpload.js.map +1 -0
  781. package/dist/continuum/pull.d.ts +22 -0
  782. package/dist/continuum/pull.js +47 -0
  783. package/dist/continuum/pull.js.map +1 -0
  784. package/dist/continuum/sourceBundle.d.ts +18 -0
  785. package/dist/continuum/sourceBundle.js +160 -0
  786. package/dist/continuum/sourceBundle.js.map +1 -0
  787. package/dist/continuum/state.d.ts +41 -0
  788. package/dist/continuum/state.js +198 -0
  789. package/dist/continuum/state.js.map +1 -0
  790. package/dist/continuum/types.d.ts +156 -0
  791. package/dist/continuum/types.js +6 -0
  792. package/dist/continuum/types.js.map +1 -0
  793. package/dist/continuum/workflow.d.ts +62 -0
  794. package/dist/continuum/workflow.js +592 -0
  795. package/dist/continuum/workflow.js.map +1 -0
  796. package/dist/cs2/areas.d.ts +41 -0
  797. package/dist/cs2/areas.js +82 -0
  798. package/dist/cs2/areas.js.map +1 -0
  799. package/dist/cs2/binary.d.ts +10 -0
  800. package/dist/cs2/binary.js +59 -0
  801. package/dist/cs2/binary.js.map +1 -0
  802. package/dist/cs2/cityLegacy.d.ts +29 -0
  803. package/dist/cs2/cityLegacy.js +48 -0
  804. package/dist/cs2/cityLegacy.js.map +1 -0
  805. package/dist/cs2/convert/areas.d.ts +32 -0
  806. package/dist/cs2/convert/areas.js +96 -0
  807. package/dist/cs2/convert/areas.js.map +1 -0
  808. package/dist/cs2/convert/glb.d.ts +48 -0
  809. package/dist/cs2/convert/glb.js +178 -0
  810. package/dist/cs2/convert/glb.js.map +1 -0
  811. package/dist/cs2/convert/markings.d.ts +96 -0
  812. package/dist/cs2/convert/markings.js +329 -0
  813. package/dist/cs2/convert/markings.js.map +1 -0
  814. package/dist/cs2/convert/nodeSeams.d.ts +20 -0
  815. package/dist/cs2/convert/nodeSeams.js +164 -0
  816. package/dist/cs2/convert/nodeSeams.js.map +1 -0
  817. package/dist/cs2/convert/roadBake.d.ts +112 -0
  818. package/dist/cs2/convert/roadBake.js +290 -0
  819. package/dist/cs2/convert/roadBake.js.map +1 -0
  820. package/dist/cs2/convert/roads.d.ts +110 -0
  821. package/dist/cs2/convert/roads.js +419 -0
  822. package/dist/cs2/convert/roads.js.map +1 -0
  823. package/dist/cs2/convert/terrain.d.ts +67 -0
  824. package/dist/cs2/convert/terrain.js +141 -0
  825. package/dist/cs2/convert/terrain.js.map +1 -0
  826. package/dist/cs2/convert/terrainConform.d.ts +58 -0
  827. package/dist/cs2/convert/terrainConform.js +284 -0
  828. package/dist/cs2/convert/terrainConform.js.map +1 -0
  829. package/dist/cs2/convert/treeRoadClearance.d.ts +17 -0
  830. package/dist/cs2/convert/treeRoadClearance.js +78 -0
  831. package/dist/cs2/convert/treeRoadClearance.js.map +1 -0
  832. package/dist/cs2/convert/trees.d.ts +33 -0
  833. package/dist/cs2/convert/trees.js +68 -0
  834. package/dist/cs2/convert/trees.js.map +1 -0
  835. package/dist/cs2/csm2.d.ts +58 -0
  836. package/dist/cs2/csm2.js +188 -0
  837. package/dist/cs2/csm2.js.map +1 -0
  838. package/dist/cs2/fixtures.d.ts +5 -0
  839. package/dist/cs2/fixtures.js +15 -0
  840. package/dist/cs2/fixtures.js.map +1 -0
  841. package/dist/cs2/impostors.d.ts +31 -0
  842. package/dist/cs2/impostors.js +42 -0
  843. package/dist/cs2/impostors.js.map +1 -0
  844. package/dist/cs2/index.d.ts +22 -0
  845. package/dist/cs2/index.js +191 -0
  846. package/dist/cs2/index.js.map +1 -0
  847. package/dist/cs2/inspect.d.ts +81 -0
  848. package/dist/cs2/inspect.js +181 -0
  849. package/dist/cs2/inspect.js.map +1 -0
  850. package/dist/cs2/lods.d.ts +22 -0
  851. package/dist/cs2/lods.js +136 -0
  852. package/dist/cs2/lods.js.map +1 -0
  853. package/dist/cs2/materials.d.ts +43 -0
  854. package/dist/cs2/materials.js +137 -0
  855. package/dist/cs2/materials.js.map +1 -0
  856. package/dist/cs2/placements.d.ts +57 -0
  857. package/dist/cs2/placements.js +116 -0
  858. package/dist/cs2/placements.js.map +1 -0
  859. package/dist/cs2/roadPieces.d.ts +79 -0
  860. package/dist/cs2/roadPieces.js +263 -0
  861. package/dist/cs2/roadPieces.js.map +1 -0
  862. package/dist/cs2/roads.d.ts +92 -0
  863. package/dist/cs2/roads.js +329 -0
  864. package/dist/cs2/roads.js.map +1 -0
  865. package/dist/cs2/terrain.d.ts +37 -0
  866. package/dist/cs2/terrain.js +91 -0
  867. package/dist/cs2/terrain.js.map +1 -0
  868. package/dist/cs2/types.d.ts +444 -0
  869. package/dist/cs2/types.js +22 -0
  870. package/dist/cs2/types.js.map +1 -0
  871. package/dist/cs2/water.d.ts +26 -0
  872. package/dist/cs2/water.js +58 -0
  873. package/dist/cs2/water.js.map +1 -0
  874. package/dist/currency.d.ts +94 -0
  875. package/dist/currency.js +425 -0
  876. package/dist/currency.js.map +1 -0
  877. package/dist/dev/devServer.d.ts +53 -0
  878. package/dist/dev/devServer.js +228 -0
  879. package/dist/dev/devServer.js.map +1 -0
  880. package/dist/dev/shellPage.d.ts +3 -0
  881. package/dist/dev/shellPage.js +297 -0
  882. package/dist/dev/shellPage.js.map +1 -0
  883. package/dist/dev/state.d.ts +84 -0
  884. package/dist/dev/state.js +265 -0
  885. package/dist/dev/state.js.map +1 -0
  886. package/dist/device.d.ts +104 -0
  887. package/dist/device.js +2744 -0
  888. package/dist/device.js.map +1 -0
  889. package/dist/deviceActionValidation.d.ts +21 -0
  890. package/dist/deviceActionValidation.js +460 -0
  891. package/dist/deviceActionValidation.js.map +1 -0
  892. package/dist/distribution.d.ts +69 -0
  893. package/dist/distribution.js +138 -0
  894. package/dist/distribution.js.map +1 -0
  895. package/dist/enginePins.d.ts +45 -0
  896. package/dist/enginePins.js +134 -0
  897. package/dist/enginePins.js.map +1 -0
  898. package/dist/gesture/axisReference.d.ts +22 -0
  899. package/dist/gesture/axisReference.js +32 -0
  900. package/dist/gesture/axisReference.js.map +1 -0
  901. package/dist/gesture/capture.d.ts +84 -0
  902. package/dist/gesture/capture.js +303 -0
  903. package/dist/gesture/capture.js.map +1 -0
  904. package/dist/gesture/harnessPage.d.ts +23 -0
  905. package/dist/gesture/harnessPage.js +153 -0
  906. package/dist/gesture/harnessPage.js.map +1 -0
  907. package/dist/gesture/inspect.d.ts +189 -0
  908. package/dist/gesture/inspect.js +678 -0
  909. package/dist/gesture/inspect.js.map +1 -0
  910. package/dist/gesture/rig.d.ts +12 -0
  911. package/dist/gesture/rig.js +111 -0
  912. package/dist/gesture/rig.js.map +1 -0
  913. package/dist/gesture/worldRuntime.d.ts +65 -0
  914. package/dist/gesture/worldRuntime.js +110 -0
  915. package/dist/gesture/worldRuntime.js.map +1 -0
  916. package/dist/glb/draw-calls.d.ts +60 -0
  917. package/dist/glb/draw-calls.js +160 -0
  918. package/dist/glb/draw-calls.js.map +1 -0
  919. package/dist/glb/glb-summary.d.ts +68 -0
  920. package/dist/glb/glb-summary.js +450 -0
  921. package/dist/glb/glb-summary.js.map +1 -0
  922. package/dist/glb/index.d.ts +12 -0
  923. package/dist/glb/index.js +24 -0
  924. package/dist/glb/index.js.map +1 -0
  925. package/dist/glb/smooth-normals.d.ts +98 -0
  926. package/dist/glb/smooth-normals.js +395 -0
  927. package/dist/glb/smooth-normals.js.map +1 -0
  928. package/dist/index.d.ts +1 -1
  929. package/dist/index.js +6786 -128
  930. package/dist/index.js.map +1 -1
  931. package/dist/init.d.ts +35 -1
  932. package/dist/init.js +1115 -47
  933. package/dist/init.js.map +1 -1
  934. package/dist/install.d.ts +16 -5
  935. package/dist/install.js +123 -23
  936. package/dist/install.js.map +1 -1
  937. package/dist/interactable.d.ts +154 -0
  938. package/dist/interactable.js +1219 -0
  939. package/dist/interactable.js.map +1 -0
  940. package/dist/item.d.ts +217 -0
  941. package/dist/item.js +892 -0
  942. package/dist/item.js.map +1 -0
  943. package/dist/itemDef/distribute.d.ts +17 -0
  944. package/dist/itemDef/distribute.js +29 -0
  945. package/dist/itemDef/distribute.js.map +1 -0
  946. package/dist/itemDef/emoteRules.d.ts +85 -0
  947. package/dist/itemDef/emoteRules.js +368 -0
  948. package/dist/itemDef/emoteRules.js.map +1 -0
  949. package/dist/itemDef/gunConfigRules.d.ts +24 -0
  950. package/dist/itemDef/gunConfigRules.js +300 -0
  951. package/dist/itemDef/gunConfigRules.js.map +1 -0
  952. package/dist/itemDef/inspect.d.ts +19 -0
  953. package/dist/itemDef/inspect.js +144 -0
  954. package/dist/itemDef/inspect.js.map +1 -0
  955. package/dist/itemDef/scaffold.d.ts +41 -0
  956. package/dist/itemDef/scaffold.js +303 -0
  957. package/dist/itemDef/scaffold.js.map +1 -0
  958. package/dist/itemDef/tables.d.ts +81 -0
  959. package/dist/itemDef/tables.js +226 -0
  960. package/dist/itemDef/tables.js.map +1 -0
  961. package/dist/itemDef/validate.d.ts +30 -0
  962. package/dist/itemDef/validate.js +175 -0
  963. package/dist/itemDef/validate.js.map +1 -0
  964. package/dist/itemPublishVersion.d.ts +46 -0
  965. package/dist/itemPublishVersion.js +242 -0
  966. package/dist/itemPublishVersion.js.map +1 -0
  967. package/dist/itemSearch.d.ts +128 -0
  968. package/dist/itemSearch.js +140 -0
  969. package/dist/itemSearch.js.map +1 -0
  970. package/dist/itemSetVersion.d.ts +42 -0
  971. package/dist/itemSetVersion.js +94 -0
  972. package/dist/itemSetVersion.js.map +1 -0
  973. package/dist/itemThumbnail.d.ts +40 -0
  974. package/dist/itemThumbnail.js +116 -0
  975. package/dist/itemThumbnail.js.map +1 -0
  976. package/dist/lib.d.ts +571 -7
  977. package/dist/lib.js +1178 -15
  978. package/dist/lib.js.map +1 -1
  979. package/dist/looks.d.ts +103 -0
  980. package/dist/looks.js +102 -0
  981. package/dist/looks.js.map +1 -0
  982. package/dist/materials/availability.d.ts +8 -0
  983. package/dist/materials/availability.js +22 -0
  984. package/dist/materials/availability.js.map +1 -0
  985. package/dist/materials/identity.d.ts +30 -0
  986. package/dist/materials/identity.js +68 -0
  987. package/dist/materials/identity.js.map +1 -0
  988. package/dist/materials/pins.d.ts +23 -0
  989. package/dist/materials/pins.js +93 -0
  990. package/dist/materials/pins.js.map +1 -0
  991. package/dist/mesh/surfaceBvh.d.ts +13 -0
  992. package/dist/mesh/surfaceBvh.js +197 -0
  993. package/dist/mesh/surfaceBvh.js.map +1 -0
  994. package/dist/module-validation.d.ts +2 -0
  995. package/dist/module-validation.js +24 -0
  996. package/dist/module-validation.js.map +1 -0
  997. package/dist/money.d.ts +183 -0
  998. package/dist/money.js +299 -0
  999. package/dist/money.js.map +1 -0
  1000. package/dist/npc/capture.d.ts +41 -0
  1001. package/dist/npc/capture.js +99 -0
  1002. package/dist/npc/capture.js.map +1 -0
  1003. package/dist/npc/format.d.ts +2 -0
  1004. package/dist/npc/format.js +102 -0
  1005. package/dist/npc/format.js.map +1 -0
  1006. package/dist/npc/inspect.d.ts +23 -0
  1007. package/dist/npc/inspect.js +54 -0
  1008. package/dist/npc/inspect.js.map +1 -0
  1009. package/dist/npc/metrics.d.ts +132 -0
  1010. package/dist/npc/metrics.js +214 -0
  1011. package/dist/npc/metrics.js.map +1 -0
  1012. package/dist/npc/session.d.ts +35 -0
  1013. package/dist/npc/session.js +172 -0
  1014. package/dist/npc/session.js.map +1 -0
  1015. package/dist/pack/locomotionReference.d.ts +40 -0
  1016. package/dist/pack/locomotionReference.js +132 -0
  1017. package/dist/pack/locomotionReference.js.map +1 -0
  1018. package/dist/pack/scaffold.d.ts +15 -0
  1019. package/dist/pack/scaffold.js +144 -0
  1020. package/dist/pack/scaffold.js.map +1 -0
  1021. package/dist/pack/validate.d.ts +67 -0
  1022. package/dist/pack/validate.js +317 -0
  1023. package/dist/pack/validate.js.map +1 -0
  1024. package/dist/package.d.ts +14 -1
  1025. package/dist/package.js +106 -7
  1026. package/dist/package.js.map +1 -1
  1027. package/dist/paletteShowcase.d.ts +183 -0
  1028. package/dist/paletteShowcase.js +454 -0
  1029. package/dist/paletteShowcase.js.map +1 -0
  1030. package/dist/paletteShowcaseProfiles.d.ts +112 -0
  1031. package/dist/paletteShowcaseProfiles.js +121 -0
  1032. package/dist/paletteShowcaseProfiles.js.map +1 -0
  1033. package/dist/paths.d.ts +4 -0
  1034. package/dist/paths.js +11 -0
  1035. package/dist/paths.js.map +1 -0
  1036. package/dist/placementRequest.d.ts +37 -0
  1037. package/dist/placementRequest.js +220 -0
  1038. package/dist/placementRequest.js.map +1 -0
  1039. package/dist/placements/assets.d.ts +7 -0
  1040. package/dist/placements/assets.js +114 -0
  1041. package/dist/placements/assets.js.map +1 -0
  1042. package/dist/placements/attachFoliageLods.d.ts +4 -0
  1043. package/dist/placements/attachFoliageLods.js +70 -0
  1044. package/dist/placements/attachFoliageLods.js.map +1 -0
  1045. package/dist/placements/attachImpostors.d.ts +3 -0
  1046. package/dist/placements/attachImpostors.js +51 -0
  1047. package/dist/placements/attachImpostors.js.map +1 -0
  1048. package/dist/placements/authored.d.ts +11 -0
  1049. package/dist/placements/authored.js +65 -0
  1050. package/dist/placements/authored.js.map +1 -0
  1051. package/dist/placements/bake.d.ts +2 -0
  1052. package/dist/placements/bake.js +235 -0
  1053. package/dist/placements/bake.js.map +1 -0
  1054. package/dist/placements/captureHost.d.ts +4 -0
  1055. package/dist/placements/captureHost.js +125 -0
  1056. package/dist/placements/captureHost.js.map +1 -0
  1057. package/dist/placements/foliageCapture.d.ts +2 -0
  1058. package/dist/placements/foliageCapture.js +98 -0
  1059. package/dist/placements/foliageCapture.js.map +1 -0
  1060. package/dist/placements/foliageCards.d.ts +17 -0
  1061. package/dist/placements/foliageCards.js +120 -0
  1062. package/dist/placements/foliageCards.js.map +1 -0
  1063. package/dist/placements/foliageLods.d.ts +8 -0
  1064. package/dist/placements/foliageLods.js +240 -0
  1065. package/dist/placements/foliageLods.js.map +1 -0
  1066. package/dist/placements/foliageTexture.d.ts +4 -0
  1067. package/dist/placements/foliageTexture.js +91 -0
  1068. package/dist/placements/foliageTexture.js.map +1 -0
  1069. package/dist/placements/impostorCapture.d.ts +2 -0
  1070. package/dist/placements/impostorCapture.js +111 -0
  1071. package/dist/placements/impostorCapture.js.map +1 -0
  1072. package/dist/placements/impostors.d.ts +18 -0
  1073. package/dist/placements/impostors.js +279 -0
  1074. package/dist/placements/impostors.js.map +1 -0
  1075. package/dist/placements/source.d.ts +53 -0
  1076. package/dist/placements/source.js +111 -0
  1077. package/dist/placements/source.js.map +1 -0
  1078. package/dist/product.d.ts +84 -0
  1079. package/dist/product.js +285 -0
  1080. package/dist/product.js.map +1 -0
  1081. package/dist/publications.d.ts +116 -0
  1082. package/dist/publications.js +180 -0
  1083. package/dist/publications.js.map +1 -0
  1084. package/dist/publishDuplicateGuard.d.ts +92 -0
  1085. package/dist/publishDuplicateGuard.js +128 -0
  1086. package/dist/publishDuplicateGuard.js.map +1 -0
  1087. package/dist/realizability.d.ts +36 -0
  1088. package/dist/realizability.js +269 -0
  1089. package/dist/realizability.js.map +1 -0
  1090. package/dist/resolve.d.ts +7 -1
  1091. package/dist/resolve.js +77 -11
  1092. package/dist/resolve.js.map +1 -1
  1093. package/dist/sceneElements/compiler.d.ts +21 -0
  1094. package/dist/sceneElements/compiler.js +976 -0
  1095. package/dist/sceneElements/compiler.js.map +1 -0
  1096. package/dist/sceneElements/continuum.d.ts +54 -0
  1097. package/dist/sceneElements/continuum.js +451 -0
  1098. package/dist/sceneElements/continuum.js.map +1 -0
  1099. package/dist/sceneElements/devices.d.ts +12 -0
  1100. package/dist/sceneElements/devices.js +212 -0
  1101. package/dist/sceneElements/devices.js.map +1 -0
  1102. package/dist/sceneElements/meshCells.d.ts +49 -0
  1103. package/dist/sceneElements/meshCells.js +174 -0
  1104. package/dist/sceneElements/meshCells.js.map +1 -0
  1105. package/dist/sceneElements/types.d.ts +261 -0
  1106. package/dist/sceneElements/types.js +3 -0
  1107. package/dist/sceneElements/types.js.map +1 -0
  1108. package/dist/sceneHlod/approximate.d.ts +23 -0
  1109. package/dist/sceneHlod/approximate.js +171 -0
  1110. package/dist/sceneHlod/approximate.js.map +1 -0
  1111. package/dist/sceneHlod/artifact.d.ts +3 -0
  1112. package/dist/sceneHlod/artifact.js +29 -0
  1113. package/dist/sceneHlod/artifact.js.map +1 -0
  1114. package/dist/sceneHlod/bakeSurface.d.ts +2 -0
  1115. package/dist/sceneHlod/bakeSurface.js +59 -0
  1116. package/dist/sceneHlod/bakeSurface.js.map +1 -0
  1117. package/dist/sceneHlod/basisEncoder.d.ts +3 -0
  1118. package/dist/sceneHlod/basisEncoder.js +68 -0
  1119. package/dist/sceneHlod/basisEncoder.js.map +1 -0
  1120. package/dist/sceneHlod/cellArtifact.d.ts +17 -0
  1121. package/dist/sceneHlod/cellArtifact.js +78 -0
  1122. package/dist/sceneHlod/cellArtifact.js.map +1 -0
  1123. package/dist/sceneHlod/commands.d.ts +2 -0
  1124. package/dist/sceneHlod/commands.js +114 -0
  1125. package/dist/sceneHlod/commands.js.map +1 -0
  1126. package/dist/sceneHlod/complete.d.ts +78 -0
  1127. package/dist/sceneHlod/complete.js +261 -0
  1128. package/dist/sceneHlod/complete.js.map +1 -0
  1129. package/dist/sceneHlod/completePrototype.d.ts +39 -0
  1130. package/dist/sceneHlod/completePrototype.js +121 -0
  1131. package/dist/sceneHlod/completePrototype.js.map +1 -0
  1132. package/dist/sceneHlod/geometry.d.ts +6 -0
  1133. package/dist/sceneHlod/geometry.js +101 -0
  1134. package/dist/sceneHlod/geometry.js.map +1 -0
  1135. package/dist/sceneHlod/imageSampling.d.ts +11 -0
  1136. package/dist/sceneHlod/imageSampling.js +100 -0
  1137. package/dist/sceneHlod/imageSampling.js.map +1 -0
  1138. package/dist/sceneHlod/refinement.d.ts +46 -0
  1139. package/dist/sceneHlod/refinement.js +204 -0
  1140. package/dist/sceneHlod/refinement.js.map +1 -0
  1141. package/dist/sceneHlod/scene.d.ts +80 -0
  1142. package/dist/sceneHlod/scene.js +264 -0
  1143. package/dist/sceneHlod/scene.js.map +1 -0
  1144. package/dist/sceneHlod/sharedAtlas.d.ts +37 -0
  1145. package/dist/sceneHlod/sharedAtlas.js +84 -0
  1146. package/dist/sceneHlod/sharedAtlas.js.map +1 -0
  1147. package/dist/sceneHlod/source.d.ts +17 -0
  1148. package/dist/sceneHlod/source.js +241 -0
  1149. package/dist/sceneHlod/source.js.map +1 -0
  1150. package/dist/sceneHlod/textureCodec.d.ts +4 -0
  1151. package/dist/sceneHlod/textureCodec.js +66 -0
  1152. package/dist/sceneHlod/textureCodec.js.map +1 -0
  1153. package/dist/sceneHlod/transfer.d.ts +8 -0
  1154. package/dist/sceneHlod/transfer.js +147 -0
  1155. package/dist/sceneHlod/transfer.js.map +1 -0
  1156. package/dist/sceneHlod/types.d.ts +69 -0
  1157. package/dist/sceneHlod/types.js +3 -0
  1158. package/dist/sceneHlod/types.js.map +1 -0
  1159. package/dist/sceneImport/archive.d.ts +30 -0
  1160. package/dist/sceneImport/archive.js +336 -0
  1161. package/dist/sceneImport/archive.js.map +1 -0
  1162. package/dist/sceneImport/bistroMaterialBake.d.ts +20 -0
  1163. package/dist/sceneImport/bistroMaterialBake.js +169 -0
  1164. package/dist/sceneImport/bistroMaterialBake.js.map +1 -0
  1165. package/dist/sceneImport/bistroTexturePlan.d.ts +6 -0
  1166. package/dist/sceneImport/bistroTexturePlan.js +23 -0
  1167. package/dist/sceneImport/bistroTexturePlan.js.map +1 -0
  1168. package/dist/sceneImport/compiler.d.ts +36 -0
  1169. package/dist/sceneImport/compiler.js +251 -0
  1170. package/dist/sceneImport/compiler.js.map +1 -0
  1171. package/dist/sceneImport/composeMounts.d.ts +159 -0
  1172. package/dist/sceneImport/composeMounts.js +687 -0
  1173. package/dist/sceneImport/composeMounts.js.map +1 -0
  1174. package/dist/sceneImport/defaultMaterials.d.ts +45 -0
  1175. package/dist/sceneImport/defaultMaterials.js +121 -0
  1176. package/dist/sceneImport/defaultMaterials.js.map +1 -0
  1177. package/dist/sceneImport/externalTextures.d.ts +44 -0
  1178. package/dist/sceneImport/externalTextures.js +146 -0
  1179. package/dist/sceneImport/externalTextures.js.map +1 -0
  1180. package/dist/sceneImport/fbx.d.ts +101 -0
  1181. package/dist/sceneImport/fbx.js +307 -0
  1182. package/dist/sceneImport/fbx.js.map +1 -0
  1183. package/dist/sceneImport/glbSplit.d.ts +24 -0
  1184. package/dist/sceneImport/glbSplit.js +95 -0
  1185. package/dist/sceneImport/glbSplit.js.map +1 -0
  1186. package/dist/sceneImport/gltfIo.d.ts +32 -0
  1187. package/dist/sceneImport/gltfIo.js +39 -0
  1188. package/dist/sceneImport/gltfIo.js.map +1 -0
  1189. package/dist/sceneImport/helsinkiTiles.d.ts +31 -0
  1190. package/dist/sceneImport/helsinkiTiles.js +52 -0
  1191. package/dist/sceneImport/helsinkiTiles.js.map +1 -0
  1192. package/dist/sceneImport/materials/binding.d.ts +16 -0
  1193. package/dist/sceneImport/materials/binding.js +42 -0
  1194. package/dist/sceneImport/materials/binding.js.map +1 -0
  1195. package/dist/sceneImport/materials/catalog.d.ts +36 -0
  1196. package/dist/sceneImport/materials/catalog.js +233 -0
  1197. package/dist/sceneImport/materials/catalog.js.map +1 -0
  1198. package/dist/sceneImport/materials/index.d.ts +4 -0
  1199. package/dist/sceneImport/materials/index.js +21 -0
  1200. package/dist/sceneImport/materials/index.js.map +1 -0
  1201. package/dist/sceneImport/materials/matcher.d.ts +33 -0
  1202. package/dist/sceneImport/materials/matcher.js +127 -0
  1203. package/dist/sceneImport/materials/matcher.js.map +1 -0
  1204. package/dist/sceneImport/materials/recipe.d.ts +30 -0
  1205. package/dist/sceneImport/materials/recipe.js +134 -0
  1206. package/dist/sceneImport/materials/recipe.js.map +1 -0
  1207. package/dist/sceneImport/multiAsset.d.ts +79 -0
  1208. package/dist/sceneImport/multiAsset.js +535 -0
  1209. package/dist/sceneImport/multiAsset.js.map +1 -0
  1210. package/dist/sceneImport/multiAssetSpawn.d.ts +40 -0
  1211. package/dist/sceneImport/multiAssetSpawn.js +98 -0
  1212. package/dist/sceneImport/multiAssetSpawn.js.map +1 -0
  1213. package/dist/sceneImport/obj.d.ts +41 -0
  1214. package/dist/sceneImport/obj.js +276 -0
  1215. package/dist/sceneImport/obj.js.map +1 -0
  1216. package/dist/sceneImport/package.d.ts +104 -0
  1217. package/dist/sceneImport/package.js +1004 -0
  1218. package/dist/sceneImport/package.js.map +1 -0
  1219. package/dist/sceneImport/packageReader.d.ts +10 -0
  1220. package/dist/sceneImport/packageReader.js +250 -0
  1221. package/dist/sceneImport/packageReader.js.map +1 -0
  1222. package/dist/sceneImport/proxy.d.ts +100 -0
  1223. package/dist/sceneImport/proxy.js +334 -0
  1224. package/dist/sceneImport/proxy.js.map +1 -0
  1225. package/dist/sceneImport/publish.d.ts +47 -0
  1226. package/dist/sceneImport/publish.js +98 -0
  1227. package/dist/sceneImport/publish.js.map +1 -0
  1228. package/dist/sceneImport/semantic.d.ts +75 -0
  1229. package/dist/sceneImport/semantic.js +296 -0
  1230. package/dist/sceneImport/semantic.js.map +1 -0
  1231. package/dist/sceneImport/spawnProbe.d.ts +55 -0
  1232. package/dist/sceneImport/spawnProbe.js +159 -0
  1233. package/dist/sceneImport/spawnProbe.js.map +1 -0
  1234. package/dist/sceneImport/streamLayout.d.ts +96 -0
  1235. package/dist/sceneImport/streamLayout.js +203 -0
  1236. package/dist/sceneImport/streamLayout.js.map +1 -0
  1237. package/dist/sceneImport/textureDecode.d.ts +21 -0
  1238. package/dist/sceneImport/textureDecode.js +292 -0
  1239. package/dist/sceneImport/textureDecode.js.map +1 -0
  1240. package/dist/sceneImport/types.d.ts +84 -0
  1241. package/dist/sceneImport/types.js +58 -0
  1242. package/dist/sceneImport/types.js.map +1 -0
  1243. package/dist/scenePerformance.d.ts +162 -0
  1244. package/dist/scenePerformance.js +905 -0
  1245. package/dist/scenePerformance.js.map +1 -0
  1246. package/dist/screenshot/captureScreenshots.d.ts +36 -0
  1247. package/dist/screenshot/captureScreenshots.js +229 -0
  1248. package/dist/screenshot/captureScreenshots.js.map +1 -0
  1249. package/dist/screenshot/encodeImagePreview.d.ts +5 -0
  1250. package/dist/screenshot/encodeImagePreview.js +42 -0
  1251. package/dist/screenshot/encodeImagePreview.js.map +1 -0
  1252. package/dist/screenshot/publishThumbnail.d.ts +4 -0
  1253. package/dist/screenshot/publishThumbnail.js +56 -0
  1254. package/dist/screenshot/publishThumbnail.js.map +1 -0
  1255. package/dist/screenshot/staticServe.d.ts +14 -0
  1256. package/dist/screenshot/staticServe.js +120 -0
  1257. package/dist/screenshot/staticServe.js.map +1 -0
  1258. package/dist/source-archive.d.ts +101 -0
  1259. package/dist/source-archive.js +309 -0
  1260. package/dist/source-archive.js.map +1 -0
  1261. package/dist/stdioBlocking.d.ts +1 -0
  1262. package/dist/stdioBlocking.js +9 -0
  1263. package/dist/stdioBlocking.js.map +1 -0
  1264. package/dist/terrain/bake.d.ts +40 -0
  1265. package/dist/terrain/bake.js +318 -0
  1266. package/dist/terrain/bake.js.map +1 -0
  1267. package/dist/terrain/generate.d.ts +21 -0
  1268. package/dist/terrain/generate.js +239 -0
  1269. package/dist/terrain/generate.js.map +1 -0
  1270. package/dist/terrain/grid.d.ts +48 -0
  1271. package/dist/terrain/grid.js +133 -0
  1272. package/dist/terrain/grid.js.map +1 -0
  1273. package/dist/terrain/import.d.ts +40 -0
  1274. package/dist/terrain/import.js +252 -0
  1275. package/dist/terrain/import.js.map +1 -0
  1276. package/dist/terrain/index.d.ts +11 -0
  1277. package/dist/terrain/index.js +88 -0
  1278. package/dist/terrain/index.js.map +1 -0
  1279. package/dist/terrain/inspect.d.ts +35 -0
  1280. package/dist/terrain/inspect.js +115 -0
  1281. package/dist/terrain/inspect.js.map +1 -0
  1282. package/dist/terrain/island.d.ts +31 -0
  1283. package/dist/terrain/island.js +128 -0
  1284. package/dist/terrain/island.js.map +1 -0
  1285. package/dist/terrain/noise.d.ts +79 -0
  1286. package/dist/terrain/noise.js +162 -0
  1287. package/dist/terrain/noise.js.map +1 -0
  1288. package/dist/terrain/normals.d.ts +21 -0
  1289. package/dist/terrain/normals.js +55 -0
  1290. package/dist/terrain/normals.js.map +1 -0
  1291. package/dist/terrain/presets/alpine.json +20 -0
  1292. package/dist/terrain/presets/coast.json +20 -0
  1293. package/dist/terrain/presets/desert.json +22 -0
  1294. package/dist/terrain/presets/forest.json +20 -0
  1295. package/dist/terrain/presets/meadow.json +20 -0
  1296. package/dist/terrain/presets/volcanic.json +22 -0
  1297. package/dist/terrain/presets.d.ts +29 -0
  1298. package/dist/terrain/presets.js +128 -0
  1299. package/dist/terrain/presets.js.map +1 -0
  1300. package/dist/terrain/source.d.ts +42 -0
  1301. package/dist/terrain/source.js +241 -0
  1302. package/dist/terrain/source.js.map +1 -0
  1303. package/dist/terrain/tile.d.ts +92 -0
  1304. package/dist/terrain/tile.js +268 -0
  1305. package/dist/terrain/tile.js.map +1 -0
  1306. package/dist/textureAudit.d.ts +23 -0
  1307. package/dist/textureAudit.js +192 -0
  1308. package/dist/textureAudit.js.map +1 -0
  1309. package/dist/tsconfig.build.tsbuildinfo +1 -1
  1310. package/dist/unreal/avatarMode.d.ts +21 -0
  1311. package/dist/unreal/avatarMode.js +33 -0
  1312. package/dist/unreal/avatarMode.js.map +1 -0
  1313. package/dist/unreal/delivery.d.ts +46 -0
  1314. package/dist/unreal/delivery.js +89 -0
  1315. package/dist/unreal/delivery.js.map +1 -0
  1316. package/dist/unreal/doctor.d.ts +50 -0
  1317. package/dist/unreal/doctor.js +102 -0
  1318. package/dist/unreal/doctor.js.map +1 -0
  1319. package/dist/unreal/package.d.ts +80 -0
  1320. package/dist/unreal/package.js +273 -0
  1321. package/dist/unreal/package.js.map +1 -0
  1322. package/dist/unreal/release.d.ts +41 -0
  1323. package/dist/unreal/release.js +122 -0
  1324. package/dist/unreal/release.js.map +1 -0
  1325. package/dist/unreal.d.ts +30 -0
  1326. package/dist/unreal.js +362 -0
  1327. package/dist/unreal.js.map +1 -0
  1328. package/dist/unrealCandidate.d.ts +213 -0
  1329. package/dist/unrealCandidate.js +765 -0
  1330. package/dist/unrealCandidate.js.map +1 -0
  1331. package/dist/unrealManifest.d.ts +74 -0
  1332. package/dist/unrealManifest.js +354 -0
  1333. package/dist/unrealManifest.js.map +1 -0
  1334. package/dist/unrealPublish.d.ts +111 -0
  1335. package/dist/unrealPublish.js +331 -0
  1336. package/dist/unrealPublish.js.map +1 -0
  1337. package/dist/vault.d.ts +121 -0
  1338. package/dist/vault.js +136 -0
  1339. package/dist/vault.js.map +1 -0
  1340. package/dist/vehicle/addOnDefinition.d.ts +19 -0
  1341. package/dist/vehicle/addOnDefinition.js +21 -0
  1342. package/dist/vehicle/addOnDefinition.js.map +1 -0
  1343. package/dist/vehicle/addon.d.ts +158 -0
  1344. package/dist/vehicle/addon.js +441 -0
  1345. package/dist/vehicle/addon.js.map +1 -0
  1346. package/dist/vehicle/addonDefinitionGuard.d.ts +53 -0
  1347. package/dist/vehicle/addonDefinitionGuard.js +206 -0
  1348. package/dist/vehicle/addonDefinitionGuard.js.map +1 -0
  1349. package/dist/vehicle/addonPublished.d.ts +80 -0
  1350. package/dist/vehicle/addonPublished.js +221 -0
  1351. package/dist/vehicle/addonPublished.js.map +1 -0
  1352. package/dist/vehicle/audio.d.ts +259 -0
  1353. package/dist/vehicle/audio.js +1069 -0
  1354. package/dist/vehicle/audio.js.map +1 -0
  1355. package/dist/vehicle/audioDefects.d.ts +125 -0
  1356. package/dist/vehicle/audioDefects.js +591 -0
  1357. package/dist/vehicle/audioDefects.js.map +1 -0
  1358. package/dist/vehicle/audioDistinct.d.ts +62 -0
  1359. package/dist/vehicle/audioDistinct.js +479 -0
  1360. package/dist/vehicle/audioDistinct.js.map +1 -0
  1361. package/dist/vehicle/audioImport.d.ts +217 -0
  1362. package/dist/vehicle/audioImport.js +610 -0
  1363. package/dist/vehicle/audioImport.js.map +1 -0
  1364. package/dist/vehicle/audioProvenance.d.ts +47 -0
  1365. package/dist/vehicle/audioProvenance.js +364 -0
  1366. package/dist/vehicle/audioProvenance.js.map +1 -0
  1367. package/dist/vehicle/audioReference.d.ts +36 -0
  1368. package/dist/vehicle/audioReference.js +176 -0
  1369. package/dist/vehicle/audioReference.js.map +1 -0
  1370. package/dist/vehicle/audioSlots.d.ts +27 -0
  1371. package/dist/vehicle/audioSlots.js +43 -0
  1372. package/dist/vehicle/audioSlots.js.map +1 -0
  1373. package/dist/vehicle/audioSpectral.d.ts +71 -0
  1374. package/dist/vehicle/audioSpectral.js +534 -0
  1375. package/dist/vehicle/audioSpectral.js.map +1 -0
  1376. package/dist/vehicle/cabinCheck.d.ts +209 -0
  1377. package/dist/vehicle/cabinCheck.js +799 -0
  1378. package/dist/vehicle/cabinCheck.js.map +1 -0
  1379. package/dist/vehicle/drive.d.ts +66 -0
  1380. package/dist/vehicle/drive.js +228 -0
  1381. package/dist/vehicle/drive.js.map +1 -0
  1382. package/dist/vehicle/enginePolicy.d.ts +68 -0
  1383. package/dist/vehicle/enginePolicy.js +209 -0
  1384. package/dist/vehicle/enginePolicy.js.map +1 -0
  1385. package/dist/vehicle/hostFrame.d.ts +78 -0
  1386. package/dist/vehicle/hostFrame.js +136 -0
  1387. package/dist/vehicle/hostFrame.js.map +1 -0
  1388. package/dist/vehicle/inspectionSheets.d.ts +42 -0
  1389. package/dist/vehicle/inspectionSheets.js +173 -0
  1390. package/dist/vehicle/inspectionSheets.js.map +1 -0
  1391. package/dist/vehicle/keyboardLaunch.d.ts +189 -0
  1392. package/dist/vehicle/keyboardLaunch.js +340 -0
  1393. package/dist/vehicle/keyboardLaunch.js.map +1 -0
  1394. package/dist/vehicle/lampCoverage.d.ts +49 -0
  1395. package/dist/vehicle/lampCoverage.js +252 -0
  1396. package/dist/vehicle/lampCoverage.js.map +1 -0
  1397. package/dist/vehicle/mintAddOn.d.ts +110 -0
  1398. package/dist/vehicle/mintAddOn.js +321 -0
  1399. package/dist/vehicle/mintAddOn.js.map +1 -0
  1400. package/dist/vehicle/mintVehicle.d.ts +189 -0
  1401. package/dist/vehicle/mintVehicle.js +417 -0
  1402. package/dist/vehicle/mintVehicle.js.map +1 -0
  1403. package/dist/vehicle/paintHue.d.ts +89 -0
  1404. package/dist/vehicle/paintHue.js +248 -0
  1405. package/dist/vehicle/paintHue.js.map +1 -0
  1406. package/dist/vehicle/panelCoverage.d.ts +37 -0
  1407. package/dist/vehicle/panelCoverage.js +93 -0
  1408. package/dist/vehicle/panelCoverage.js.map +1 -0
  1409. package/dist/vehicle/platform.d.ts +71 -0
  1410. package/dist/vehicle/platform.js +144 -0
  1411. package/dist/vehicle/platform.js.map +1 -0
  1412. package/dist/vehicle/preview.d.ts +339 -0
  1413. package/dist/vehicle/preview.js +854 -0
  1414. package/dist/vehicle/preview.js.map +1 -0
  1415. package/dist/vehicle/previewAddon.d.ts +197 -0
  1416. package/dist/vehicle/previewAddon.js +520 -0
  1417. package/dist/vehicle/previewAddon.js.map +1 -0
  1418. package/dist/vehicle/previewSeats.d.ts +102 -0
  1419. package/dist/vehicle/previewSeats.js +190 -0
  1420. package/dist/vehicle/previewSeats.js.map +1 -0
  1421. package/dist/vehicle/publish.d.ts +78 -0
  1422. package/dist/vehicle/publish.js +111 -0
  1423. package/dist/vehicle/publish.js.map +1 -0
  1424. package/dist/vehicle/recheckListed.d.ts +87 -0
  1425. package/dist/vehicle/recheckListed.js +200 -0
  1426. package/dist/vehicle/recheckListed.js.map +1 -0
  1427. package/dist/vehicle/renditionManifest.d.ts +10 -0
  1428. package/dist/vehicle/renditionManifest.js +56 -0
  1429. package/dist/vehicle/renditionManifest.js.map +1 -0
  1430. package/dist/vehicle/sealCheck.d.ts +30 -0
  1431. package/dist/vehicle/sealCheck.js +59 -0
  1432. package/dist/vehicle/sealCheck.js.map +1 -0
  1433. package/dist/vehicle/simulate.d.ts +219 -0
  1434. package/dist/vehicle/simulate.js +536 -0
  1435. package/dist/vehicle/simulate.js.map +1 -0
  1436. package/dist/vehicle/simulateRuntime.d.ts +76 -0
  1437. package/dist/vehicle/simulateRuntime.js +252 -0
  1438. package/dist/vehicle/simulateRuntime.js.map +1 -0
  1439. package/dist/vehicle/testutil/sourcedAudio.d.ts +13 -0
  1440. package/dist/vehicle/testutil/sourcedAudio.js +96 -0
  1441. package/dist/vehicle/testutil/sourcedAudio.js.map +1 -0
  1442. package/dist/vehicle/validate.d.ts +225 -0
  1443. package/dist/vehicle/validate.js +956 -0
  1444. package/dist/vehicle/validate.js.map +1 -0
  1445. package/dist/vehicle/wheelDesign.d.ts +98 -0
  1446. package/dist/vehicle/wheelDesign.js +276 -0
  1447. package/dist/vehicle/wheelDesign.js.map +1 -0
  1448. package/dist/verifySubPath.d.ts +69 -0
  1449. package/dist/verifySubPath.js +458 -0
  1450. package/dist/verifySubPath.js.map +1 -0
  1451. package/dist/visualQa/animation.d.ts +57 -0
  1452. package/dist/visualQa/animation.js +503 -0
  1453. package/dist/visualQa/animation.js.map +1 -0
  1454. package/dist/visualQa/avatar.d.ts +3 -0
  1455. package/dist/visualQa/avatar.js +443 -0
  1456. package/dist/visualQa/avatar.js.map +1 -0
  1457. package/dist/visualQa/avatarMath.d.ts +180 -0
  1458. package/dist/visualQa/avatarMath.js +318 -0
  1459. package/dist/visualQa/avatarMath.js.map +1 -0
  1460. package/dist/visualQa/avatarPreview.d.ts +26 -0
  1461. package/dist/visualQa/avatarPreview.js +167 -0
  1462. package/dist/visualQa/avatarPreview.js.map +1 -0
  1463. package/dist/visualQa/avatarPreviewMath.d.ts +189 -0
  1464. package/dist/visualQa/avatarPreviewMath.js +696 -0
  1465. package/dist/visualQa/avatarPreviewMath.js.map +1 -0
  1466. package/dist/visualQa/avatarPreviewProbe.d.ts +7 -0
  1467. package/dist/visualQa/avatarPreviewProbe.js +181 -0
  1468. package/dist/visualQa/avatarPreviewProbe.js.map +1 -0
  1469. package/dist/visualQa/avatarProbe.d.ts +1 -0
  1470. package/dist/visualQa/avatarProbe.js +264 -0
  1471. package/dist/visualQa/avatarProbe.js.map +1 -0
  1472. package/dist/visualQa/browser.d.ts +116 -0
  1473. package/dist/visualQa/browser.js +817 -0
  1474. package/dist/visualQa/browser.js.map +1 -0
  1475. package/dist/visualQa/candidate.d.ts +116 -0
  1476. package/dist/visualQa/candidate.js +265 -0
  1477. package/dist/visualQa/candidate.js.map +1 -0
  1478. package/dist/visualQa/checklists.d.ts +31 -0
  1479. package/dist/visualQa/checklists.js +160 -0
  1480. package/dist/visualQa/checklists.js.map +1 -0
  1481. package/dist/visualQa/command.d.ts +110 -0
  1482. package/dist/visualQa/command.js +521 -0
  1483. package/dist/visualQa/command.js.map +1 -0
  1484. package/dist/visualQa/commonAsserts.d.ts +68 -0
  1485. package/dist/visualQa/commonAsserts.js +239 -0
  1486. package/dist/visualQa/commonAsserts.js.map +1 -0
  1487. package/dist/visualQa/contactSheet.d.ts +32 -0
  1488. package/dist/visualQa/contactSheet.js +153 -0
  1489. package/dist/visualQa/contactSheet.js.map +1 -0
  1490. package/dist/visualQa/critic.d.ts +18 -0
  1491. package/dist/visualQa/critic.js +179 -0
  1492. package/dist/visualQa/critic.js.map +1 -0
  1493. package/dist/visualQa/emotePage.d.ts +10 -0
  1494. package/dist/visualQa/emotePage.js +249 -0
  1495. package/dist/visualQa/emotePage.js.map +1 -0
  1496. package/dist/visualQa/evidenceOverlay.d.ts +74 -0
  1497. package/dist/visualQa/evidenceOverlay.js +268 -0
  1498. package/dist/visualQa/evidenceOverlay.js.map +1 -0
  1499. package/dist/visualQa/harness.d.ts +57 -0
  1500. package/dist/visualQa/harness.js +3 -0
  1501. package/dist/visualQa/harness.js.map +1 -0
  1502. package/dist/visualQa/hosted.d.ts +152 -0
  1503. package/dist/visualQa/hosted.js +367 -0
  1504. package/dist/visualQa/hosted.js.map +1 -0
  1505. package/dist/visualQa/labelLimit.d.ts +12 -0
  1506. package/dist/visualQa/labelLimit.js +31 -0
  1507. package/dist/visualQa/labelLimit.js.map +1 -0
  1508. package/dist/visualQa/mirror.d.ts +36 -0
  1509. package/dist/visualQa/mirror.js +98 -0
  1510. package/dist/visualQa/mirror.js.map +1 -0
  1511. package/dist/visualQa/moveUx.d.ts +44 -0
  1512. package/dist/visualQa/moveUx.js +103 -0
  1513. package/dist/visualQa/moveUx.js.map +1 -0
  1514. package/dist/visualQa/pinnedChrome.d.ts +63 -0
  1515. package/dist/visualQa/pinnedChrome.js +214 -0
  1516. package/dist/visualQa/pinnedChrome.js.map +1 -0
  1517. package/dist/visualQa/resolve.d.ts +88 -0
  1518. package/dist/visualQa/resolve.js +248 -0
  1519. package/dist/visualQa/resolve.js.map +1 -0
  1520. package/dist/visualQa/scene.d.ts +14 -0
  1521. package/dist/visualQa/scene.js +688 -0
  1522. package/dist/visualQa/scene.js.map +1 -0
  1523. package/dist/visualQa/shot.d.ts +2 -0
  1524. package/dist/visualQa/shot.js +27 -0
  1525. package/dist/visualQa/shot.js.map +1 -0
  1526. package/dist/visualQa/signIn.d.ts +35 -0
  1527. package/dist/visualQa/signIn.js +144 -0
  1528. package/dist/visualQa/signIn.js.map +1 -0
  1529. package/dist/visualQa/submitBudget.d.ts +25 -0
  1530. package/dist/visualQa/submitBudget.js +82 -0
  1531. package/dist/visualQa/submitBudget.js.map +1 -0
  1532. package/dist/visualQa/types.d.ts +195 -0
  1533. package/dist/visualQa/types.js +12 -0
  1534. package/dist/visualQa/types.js.map +1 -0
  1535. package/dist/visualQa/vehicle.d.ts +130 -0
  1536. package/dist/visualQa/vehicle.js +2001 -0
  1537. package/dist/visualQa/vehicle.js.map +1 -0
  1538. package/dist/visualQa/vehicleAddonProbe.d.ts +1 -0
  1539. package/dist/visualQa/vehicleAddonProbe.js +115 -0
  1540. package/dist/visualQa/vehicleAddonProbe.js.map +1 -0
  1541. package/dist/visualQa/vehicleCabinFit.d.ts +78 -0
  1542. package/dist/visualQa/vehicleCabinFit.js +55 -0
  1543. package/dist/visualQa/vehicleCabinFit.js.map +1 -0
  1544. package/dist/visualQa/vehicleClearance.d.ts +135 -0
  1545. package/dist/visualQa/vehicleClearance.js +299 -0
  1546. package/dist/visualQa/vehicleClearance.js.map +1 -0
  1547. package/dist/visualQa/vehicleDriveAlive.d.ts +55 -0
  1548. package/dist/visualQa/vehicleDriveAlive.js +103 -0
  1549. package/dist/visualQa/vehicleDriveAlive.js.map +1 -0
  1550. package/dist/visualQa/vehicleGrip.d.ts +156 -0
  1551. package/dist/visualQa/vehicleGrip.js +439 -0
  1552. package/dist/visualQa/vehicleGrip.js.map +1 -0
  1553. package/dist/visualQa/vehicleInspection.d.ts +30 -0
  1554. package/dist/visualQa/vehicleInspection.js +189 -0
  1555. package/dist/visualQa/vehicleInspection.js.map +1 -0
  1556. package/dist/visualQa/vehicleLamps.d.ts +118 -0
  1557. package/dist/visualQa/vehicleLamps.js +374 -0
  1558. package/dist/visualQa/vehicleLamps.js.map +1 -0
  1559. package/dist/visualQa/vehicleMath.d.ts +440 -0
  1560. package/dist/visualQa/vehicleMath.js +967 -0
  1561. package/dist/visualQa/vehicleMath.js.map +1 -0
  1562. package/dist/visualQa/vehicleOccupants.d.ts +66 -0
  1563. package/dist/visualQa/vehicleOccupants.js +448 -0
  1564. package/dist/visualQa/vehicleOccupants.js.map +1 -0
  1565. package/dist/visualQa/vehiclePenetration.d.ts +172 -0
  1566. package/dist/visualQa/vehiclePenetration.js +1157 -0
  1567. package/dist/visualQa/vehiclePenetration.js.map +1 -0
  1568. package/dist/visualQa/vehiclePoses.d.ts +25 -0
  1569. package/dist/visualQa/vehiclePoses.js +56 -0
  1570. package/dist/visualQa/vehiclePoses.js.map +1 -0
  1571. package/dist/visualQa/vehiclePosture.d.ts +78 -0
  1572. package/dist/visualQa/vehiclePosture.js +263 -0
  1573. package/dist/visualQa/vehiclePosture.js.map +1 -0
  1574. package/dist/visualQa/vehiclePreview.d.ts +103 -0
  1575. package/dist/visualQa/vehiclePreview.js +312 -0
  1576. package/dist/visualQa/vehiclePreview.js.map +1 -0
  1577. package/dist/visualQa/vehicleProbe.d.ts +3 -0
  1578. package/dist/visualQa/vehicleProbe.js +670 -0
  1579. package/dist/visualQa/vehicleProbe.js.map +1 -0
  1580. package/dist/visualQa/vehicleStance.d.ts +108 -0
  1581. package/dist/visualQa/vehicleStance.js +200 -0
  1582. package/dist/visualQa/vehicleStance.js.map +1 -0
  1583. package/dist/visualQa/viewer.d.ts +6 -0
  1584. package/dist/visualQa/viewer.js +750 -0
  1585. package/dist/visualQa/viewer.js.map +1 -0
  1586. package/dist/visualQa/viewerGeometry.d.ts +116 -0
  1587. package/dist/visualQa/viewerGeometry.js +476 -0
  1588. package/dist/visualQa/viewerGeometry.js.map +1 -0
  1589. package/dist/visualQa/viewerPage.d.ts +11 -0
  1590. package/dist/visualQa/viewerPage.js +414 -0
  1591. package/dist/visualQa/viewerPage.js.map +1 -0
  1592. package/dist/visualQa/webgpuSelfTest.d.ts +52 -0
  1593. package/dist/visualQa/webgpuSelfTest.js +113 -0
  1594. package/dist/visualQa/webgpuSelfTest.js.map +1 -0
  1595. package/dist/visualQa/winChrome.d.ts +92 -0
  1596. package/dist/visualQa/winChrome.js +445 -0
  1597. package/dist/visualQa/winChrome.js.map +1 -0
  1598. package/dist/visualQa/worldWalk.d.ts +59 -0
  1599. package/dist/visualQa/worldWalk.js +523 -0
  1600. package/dist/visualQa/worldWalk.js.map +1 -0
  1601. package/dist/visualQa/worldWalkMath.d.ts +263 -0
  1602. package/dist/visualQa/worldWalkMath.js +873 -0
  1603. package/dist/visualQa/worldWalkMath.js.map +1 -0
  1604. package/dist/visualQa/worldWalkProbe.d.ts +1 -0
  1605. package/dist/visualQa/worldWalkProbe.js +206 -0
  1606. package/dist/visualQa/worldWalkProbe.js.map +1 -0
  1607. package/dist/visualsConfig.d.ts +65 -0
  1608. package/dist/visualsConfig.js +1333 -0
  1609. package/dist/visualsConfig.js.map +1 -0
  1610. package/dist/voiceCommand.d.ts +11 -0
  1611. package/dist/voiceCommand.js +101 -0
  1612. package/dist/voiceCommand.js.map +1 -0
  1613. package/dist/world/analyze.d.ts +51 -0
  1614. package/dist/world/analyze.js +767 -0
  1615. package/dist/world/analyze.js.map +1 -0
  1616. package/dist/world/baseline.d.ts +33 -0
  1617. package/dist/world/baseline.js +111 -0
  1618. package/dist/world/baseline.js.map +1 -0
  1619. package/dist/world/canonicalize.d.ts +7 -0
  1620. package/dist/world/canonicalize.js +110 -0
  1621. package/dist/world/canonicalize.js.map +1 -0
  1622. package/dist/world/diff.d.ts +18 -0
  1623. package/dist/world/diff.js +58 -0
  1624. package/dist/world/diff.js.map +1 -0
  1625. package/dist/world/focus.d.ts +11 -0
  1626. package/dist/world/focus.js +161 -0
  1627. package/dist/world/focus.js.map +1 -0
  1628. package/dist/world/format.d.ts +6 -0
  1629. package/dist/world/format.js +125 -0
  1630. package/dist/world/format.js.map +1 -0
  1631. package/dist/world/readWorldMetrics.d.ts +10 -0
  1632. package/dist/world/readWorldMetrics.js +31 -0
  1633. package/dist/world/readWorldMetrics.js.map +1 -0
  1634. package/dist/world/snapshotTypes.d.ts +59 -0
  1635. package/dist/world/snapshotTypes.js +8 -0
  1636. package/dist/world/snapshotTypes.js.map +1 -0
  1637. package/dist/world/snapshotWorld.d.ts +16 -0
  1638. package/dist/world/snapshotWorld.js +142 -0
  1639. package/dist/world/snapshotWorld.js.map +1 -0
  1640. package/dist/world/wiring.d.ts +21 -0
  1641. package/dist/world/wiring.js +73 -0
  1642. package/dist/world/wiring.js.map +1 -0
  1643. package/dist/world/zones.d.ts +23 -0
  1644. package/dist/world/zones.js +134 -0
  1645. package/dist/world/zones.js.map +1 -0
  1646. package/docs/bridge.md +304 -0
  1647. package/docs/device-hand-reach.md +44 -0
  1648. package/docs/scene-hlod.md +139 -0
  1649. package/docs/vehicle-audio-import.md +299 -0
  1650. package/docs/vehicle-simulate.md +165 -0
  1651. package/docs/visual-qa-candidate-versions.md +71 -0
  1652. package/docs/visual-qa.md +462 -0
  1653. package/package.json +69 -4
  1654. package/plugins/beamng-vehicle/README.md +129 -0
  1655. package/plugins/beamng-vehicle/bin/__pycache__/beamng_abs.cpython-312.pyc +0 -0
  1656. package/plugins/beamng-vehicle/bin/__pycache__/beamng_audio.cpython-312.pyc +0 -0
  1657. package/plugins/beamng-vehicle/bin/__pycache__/beamng_engine.cpython-312.pyc +0 -0
  1658. package/plugins/beamng-vehicle/bin/__pycache__/beamng_expression.cpython-312.pyc +0 -0
  1659. package/plugins/beamng-vehicle/bin/__pycache__/beamng_jbeam.cpython-312.pyc +0 -0
  1660. package/plugins/beamng-vehicle/bin/__pycache__/beamng_materials.cpython-312.pyc +0 -0
  1661. package/plugins/beamng-vehicle/bin/__pycache__/beamng_stock_events.cpython-312.pyc +0 -0
  1662. package/plugins/beamng-vehicle/bin/__pycache__/fmod_studio.cpython-312.pyc +0 -0
  1663. package/plugins/beamng-vehicle/bin/__pycache__/fsb5.cpython-312.pyc +0 -0
  1664. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio.cpython-312.pyc +0 -0
  1665. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_ac.cpython-312.pyc +0 -0
  1666. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_beamng.cpython-312.pyc +0 -0
  1667. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_fixtures.cpython-312.pyc +0 -0
  1668. package/plugins/beamng-vehicle/bin/__pycache__/test_beamng_engine.cpython-312.pyc +0 -0
  1669. package/plugins/beamng-vehicle/bin/__pycache__/test_sim_audio.cpython-312.pyc +0 -0
  1670. package/plugins/beamng-vehicle/bin/beamng-vehicle.mjs +1295 -0
  1671. package/plugins/beamng-vehicle/bin/beamng_abs.py +408 -0
  1672. package/plugins/beamng-vehicle/bin/beamng_audio.py +394 -0
  1673. package/plugins/beamng-vehicle/bin/beamng_engine.py +184 -0
  1674. package/plugins/beamng-vehicle/bin/beamng_expression.py +373 -0
  1675. package/plugins/beamng-vehicle/bin/beamng_jbeam.py +858 -0
  1676. package/plugins/beamng-vehicle/bin/beamng_materials.py +261 -0
  1677. package/plugins/beamng-vehicle/bin/beamng_stock_events.py +171 -0
  1678. package/plugins/beamng-vehicle/bin/convert-beamng.py +1563 -0
  1679. package/plugins/beamng-vehicle/bin/fmod_studio.py +524 -0
  1680. package/plugins/beamng-vehicle/bin/fsb5.py +304 -0
  1681. package/plugins/beamng-vehicle/bin/index-beamng-common.mjs +16 -0
  1682. package/plugins/beamng-vehicle/bin/index-beamng.py +69 -0
  1683. package/plugins/beamng-vehicle/bin/lower-beamng.py +2399 -0
  1684. package/plugins/beamng-vehicle/bin/sim_audio.py +250 -0
  1685. package/plugins/beamng-vehicle/bin/sim_audio_ac.py +378 -0
  1686. package/plugins/beamng-vehicle/bin/sim_audio_beamng.py +878 -0
  1687. package/plugins/beamng-vehicle/bin/sim_audio_fixtures.py +287 -0
  1688. package/plugins/beamng-vehicle/bin/sim_audio_import.py +135 -0
  1689. package/plugins/beamng-vehicle/bin/test_beamng_engine.py +124 -0
  1690. package/plugins/beamng-vehicle/bin/test_beamng_jbeam.py +383 -0
  1691. package/plugins/beamng-vehicle/bin/test_beamng_precedence.py +134 -0
  1692. package/plugins/beamng-vehicle/bin/test_sim_audio.py +249 -0
  1693. package/plugins/beamng-vehicle/bridge-plugin.json +55 -0
  1694. package/plugins/beamng-vehicle/controller-adapters/beamng-0.39.4-abs.json +52 -0
  1695. package/plugins/beamng-vehicle/lib/common-dependency.mjs +357 -0
  1696. package/plugins/beamng-vehicle/lib/dae-scan.mjs +40 -0
  1697. package/plugins/beamng-vehicle/lib/file-identity.mjs +46 -0
  1698. package/plugins/beamng-vehicle/lib/material-semantics.mjs +312 -0
  1699. package/plugins/beamng-vehicle/lib/palette-contract.mjs +6 -0
  1700. package/plugins/beamng-vehicle/lib/vehicle-qa.mjs +567 -0
  1701. package/plugins/beamng-vehicle/lib/vehicle-source.mjs +247 -0
  1702. package/plugins/beamng-vehicle/profiles/source-compatibility.json +14 -0
  1703. package/plugins/beamng-vehicle/vendor/vehicle-package-v2.mjs +6451 -0
  1704. package/vendor/README.md +63 -0
  1705. package/vendor/ability-manifest.mjs +527 -6
  1706. package/vendor/audits/asset-credits.mjs +96 -0
  1707. package/vendor/audits/check-loaders.mjs +80 -0
  1708. package/vendor/audits/perf-gate.mjs +759 -0
  1709. package/vendor/audits/prove-live.mjs +307 -0
  1710. package/vendor/audits/source-audit.mjs +530 -0
  1711. package/vendor/audits/world-audit.mjs +439 -0
  1712. package/vendor/capture-body.glb +0 -0
  1713. package/vendor/capture-face.glb +0 -0
  1714. package/vendor/installed-index.mjs +1 -1
  1715. package/vendor/vehicle-package-v2.mjs +6451 -0
@@ -0,0 +1,2399 @@
1
+ #!/usr/bin/env python3
2
+ """Lower one exact BeamNG configuration into deterministic HELIX vehicle sidecars.
3
+
4
+ This is intentionally a source compiler, not a BeamNG runtime. Literal JBeam
5
+ values are retained where the HELIX v2 contract has an equivalent; every
6
+ fallback and unsupported feature is emitted in the provenance ledger.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import json
13
+ import math
14
+ import re
15
+ import stat
16
+ import sys
17
+ import uuid
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Iterable
20
+
21
+ from beamng_jbeam import (JBeamDocumentCache, MAX_SELECTED_PART_INDEX_BYTES,
22
+ SELECTED_PART_INDEX_SCHEMA, BRAKE_AXLE_PATTERN,
23
+ build_part_catalog, bounded_asset_paths,
24
+ decode_jbeam_table, pressure_wheel_options,
25
+ resolve_part_closure, selected_part_axle)
26
+ from beamng_expression import ExpressionResolution, resolve_beamng_expression
27
+ from beamng_abs import lower_selected_abs
28
+ from beamng_engine import derive_cylinders
29
+ from beamng_audio import (AudioExtractionError, AudioUnavailable, extract_engine_bank, locate_ffmpeg,
30
+ resolve_layers, stock_reason)
31
+
32
+ SCHEMA = "helix.beamng-functional-lowering/3"
33
+ SOURCE_SCHEMA = "helix.beamng-vehicle-source/1"
34
+ AUDIO_SLOTS = [
35
+ "engineLadder", "exhaustLadder", "idle", "startup", "shutdown", "shiftUp", "shiftDown", "clutch",
36
+ "transmissionWhine", "turboSpool", "blowOff", "intake", "revLimiter", "backfire", "suspension",
37
+ "impactLight", "impactMedium", "impactHeavy", "tyreSqueal", "reverseBeep", "horn", "doorOpen", "doorClose",
38
+ ]
39
+
40
+ # BeamNG slot types are namespaced by vehicle and frequently carry axle, side,
41
+ # or engine-variant qualifiers. Reduce only those structural qualifiers, then
42
+ # perform an exact lookup. In particular, never classify by substring:
43
+ # ``flywheel`` is not a road wheel, and unknown source slots are not body kits.
44
+ ADDON_SLOT_TYPES = {
45
+ "ecu": "vehicle.ecu@1",
46
+ "intake": "vehicle.induction@1",
47
+ "turbo": "vehicle.induction@1",
48
+ "supercharger": "vehicle.induction@1",
49
+ "engine": "vehicle.engine@1",
50
+ "engine_internals": "vehicle.engine@1",
51
+ "transaxle": "vehicle.transmission@1",
52
+ "transmission": "vehicle.transmission@1",
53
+ "gearbox": "vehicle.transmission@1",
54
+ "flywheel": "vehicle.transmission@1",
55
+ "gear_ratio": "vehicle.transmission@1",
56
+ "shifter": "vehicle.transmission@1",
57
+ "differential": "vehicle.driveline@1",
58
+ "finaldrive": "vehicle.driveline@1",
59
+ "driveshaft": "vehicle.driveline@1",
60
+ "halfshafts": "vehicle.driveline@1",
61
+ "transfer_case": "vehicle.driveline@1",
62
+ "brake": "vehicle.brakes@1",
63
+ "brakepad": "vehicle.brakes@1",
64
+ "strut": "vehicle.suspension@1",
65
+ "suspension": "vehicle.suspension@1",
66
+ "spring": "vehicle.suspension@1",
67
+ "coilover": "vehicle.suspension@1",
68
+ "swaybar": "vehicle.suspension@1",
69
+ "steering_wheel": "vehicle.steering_wheel@1",
70
+ "steering": "vehicle.steering@1",
71
+ "steer": "vehicle.steering@1",
72
+ "power_steering": "vehicle.steering@1",
73
+ "wheel": "vehicle.wheel@1",
74
+ "wheeldata": "vehicle.wheel@1",
75
+ "rim": "vehicle.wheel@1",
76
+ "tire": "vehicle.tyre@1",
77
+ "tyre": "vehicle.tyre@1",
78
+ "headlight": "vehicle.headlight@1",
79
+ "taillight": "vehicle.taillight@1",
80
+ "foglight": "vehicle.aux_light@1",
81
+ "rallylight": "vehicle.aux_light@1",
82
+ "rallylights": "vehicle.aux_light@1",
83
+ "markerlight": "vehicle.aux_light@1",
84
+ "reverselight": "vehicle.aux_light@1",
85
+ "chmsl": "vehicle.aux_light@1",
86
+ "seat": "vehicle.seats@1",
87
+ "seats": "vehicle.seats@1",
88
+ "dashboard": "vehicle.dash@1",
89
+ "dash": "vehicle.dash@1",
90
+ "gauge": "vehicle.dash@1",
91
+ "gauges": "vehicle.dash@1",
92
+ "glass": "vehicle.glass@1",
93
+ "windshield": "vehicle.glass@1",
94
+ "backlight": "vehicle.glass@1",
95
+ "doorglass": "vehicle.glass@1",
96
+ "bumper_f": "vehicle.bumper_f@1",
97
+ "bumper_r": "vehicle.bumper_r@1",
98
+ "spoiler": "vehicle.spoiler@1",
99
+ "wing": "vehicle.spoiler@1",
100
+ "exhaust": "vehicle.exhaust@1",
101
+ "horn": "vehicle.horn@1",
102
+ "licenseplate": "vehicle.plate@1",
103
+ "plate": "vehicle.plate@1",
104
+ "body": "vehicle.bodykit@1",
105
+ "door": "vehicle.bodykit@1",
106
+ "doorpanel": "vehicle.bodykit@1",
107
+ "hood": "vehicle.bodykit@1",
108
+ "trunk": "vehicle.bodykit@1",
109
+ "fender": "vehicle.bodykit@1",
110
+ "mirror": "vehicle.bodykit@1",
111
+ "quarterpanels": "vehicle.bodykit@1",
112
+ "sideskirt": "vehicle.bodykit@1",
113
+ "diffuser": "vehicle.bodykit@1",
114
+ "bumperbar": "vehicle.bodykit@1",
115
+ "lip": "vehicle.bodykit@1",
116
+ "undertray": "vehicle.bodykit@1",
117
+ "skidplate": "vehicle.bodykit@1",
118
+ "rollcage": "vehicle.bodykit@1",
119
+ "paint_design": "vehicle.bodykit@1",
120
+ "skin_interior": "vehicle.bodykit@1",
121
+ }
122
+
123
+
124
+ def normalized_addon_slot_key(source_slot: str, candidate: str) -> str:
125
+ def normalized(value: str) -> str:
126
+ camel_split = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", value)
127
+ return re.sub(r"[^a-z0-9]+", "_", camel_split.lower()).strip("_")
128
+
129
+ name = normalized(source_slot)
130
+ namespace = normalized(candidate)
131
+ if name.startswith(f"{namespace}_"):
132
+ name = name[len(namespace) + 1:]
133
+ tokens = name.split("_") if name else []
134
+ if not tokens:
135
+ return ""
136
+
137
+ # Nested engine slots contain a numeric displacement path before their
138
+ # exact leaf type, e.g. engine_2_5_ecu. No other middle tokens are admitted.
139
+ if tokens[0] == "engine" and len(tokens) > 1 and all(token.isdigit() for token in tokens[1:-1]):
140
+ if tokens[-1] in ("ecu", "intake"):
141
+ return tokens[-1]
142
+ if tokens[-1] == "internals":
143
+ return "engine_internals"
144
+
145
+ if len(tokens) >= 2 and tokens[:2] in (["gear", "ratio"], ["transfer", "case"],
146
+ ["steering", "wheel"], ["power", "steering"],
147
+ ["paint", "design"], ["skin", "interior"]):
148
+ return "_".join(tokens[:2])
149
+ if tokens[0] == "bumper" and len(tokens) > 1 and tokens[1] in ("f", "r"):
150
+ return f"bumper_{tokens[1]}"
151
+ return tokens[0]
152
+
153
+
154
+ def canonical_addon_slot(source_slot: str, candidate: str) -> str | None:
155
+ return ADDON_SLOT_TYPES.get(normalized_addon_slot_key(source_slot, candidate))
156
+
157
+
158
+ def fail(message: str) -> None:
159
+ raise ValueError(message)
160
+
161
+
162
+ def stable_uuid(seed: str) -> str:
163
+ raw = bytearray(hashlib.sha256(seed.encode()).digest()[:16])
164
+ raw[6] = (raw[6] & 0x0F) | 0x50
165
+ raw[8] = (raw[8] & 0x3F) | 0x80
166
+ return str(uuid.UUID(bytes=bytes(raw)))
167
+
168
+
169
+ def slug(value: str) -> str:
170
+ return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
171
+
172
+
173
+ def rel(path: Path, root: Path) -> str:
174
+ try:
175
+ return path.resolve().relative_to(root.resolve()).as_posix()
176
+ except ValueError:
177
+ return path.name
178
+
179
+
180
+ def literal(value: Any) -> bool:
181
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
182
+ return math.isfinite(value)
183
+ if isinstance(value, str):
184
+ return not value.startswith("$=")
185
+ return False
186
+
187
+
188
+ def sha256_file(path: Path, chunk_bytes: int = 1024 * 1024) -> tuple[str, int]:
189
+ digest = hashlib.sha256()
190
+ size = 0
191
+ with path.open("rb") as handle:
192
+ while chunk := handle.read(chunk_bytes):
193
+ digest.update(chunk)
194
+ size += len(chunk)
195
+ return digest.hexdigest(), size
196
+
197
+
198
+ def canonical_json_receipt(value: Any) -> tuple[str, int]:
199
+ digest = hashlib.sha256()
200
+ size = 0
201
+ encoder = json.JSONEncoder(sort_keys=True, separators=(",", ":"), ensure_ascii=False)
202
+ for chunk in encoder.iterencode(value):
203
+ encoded = chunk.encode("utf-8")
204
+ digest.update(encoded)
205
+ size += len(encoded)
206
+ return digest.hexdigest(), size
207
+
208
+
209
+ MAX_INDEX_SOURCES = 50_000
210
+ MAX_INDEX_PARTS = 100_000
211
+ LUA_SCAN_LIMIT = (20_000, 512 * 1024 * 1024, 16 * 1024 * 1024)
212
+
213
+
214
+ def portable_index_path(value: Any) -> str:
215
+ if not isinstance(value, str) or not value or "\\" in value or value.startswith("/"):
216
+ fail("selected-part index contains a non-portable source path")
217
+ pieces = value.split("/")
218
+ if any(piece in ("", ".", "..") for piece in pieces):
219
+ fail("selected-part index source path escapes its admitted root")
220
+ return value
221
+
222
+
223
+ def confined_file(root: Path, portable: str) -> Path:
224
+ current = root
225
+ for piece in portable_index_path(portable).split("/"):
226
+ current = current / piece
227
+ if current.is_symlink():
228
+ fail(f"selected-part index source traverses a symlink: {portable}")
229
+ try:
230
+ info = current.stat()
231
+ current.resolve().relative_to(root.resolve())
232
+ except (FileNotFoundError, ValueError):
233
+ fail(f"selected-part index source is outside its admitted root: {portable}")
234
+ if not stat.S_ISREG(info.st_mode):
235
+ fail(f"selected-part index source is not a regular file: {portable}")
236
+ return current.resolve()
237
+
238
+
239
+ WATCHED_KEYS = {"idleRPM", "idleRpm", "revLimiterRPM", "maxRPM", "limiterRpm", "inertia", "friction",
240
+ "dynamicFriction", "engineBrakeTorque", "gearRatios", "reverseGearRatio", "reverseRatio",
241
+ "gearRatio", "transmissionShiftDelay", "shiftDelay", "diffTorqueSplit", "sampleName",
242
+ "brakeTorque", "beamSpring", "beamDamp", "radius", "electricsName", "electric", "deformSound",
243
+ "breakGroup", "breakTriggerBeam", "deformGroup"}
244
+
245
+ MESH_ROLE_RULES = [
246
+ ("door", ("door",)), ("bonnet", ("hood", "bonnet")), ("boot", ("trunk", "tailgate")),
247
+ ("glass", ("glass",)), ("lamp", ("light",)), ("mirror", ("mirror",)),
248
+ ("wing", ("wing", "spoiler")), ("interior", ("seat", "dash", "interior")),
249
+ ]
250
+
251
+ PANEL_RULES = [
252
+ {"id": "doorFL", "role": "door", "slot": re.compile(r"(?:^|_)door_FL$", re.I), "openDeg": -68},
253
+ {"id": "doorFR", "role": "door", "slot": re.compile(r"(?:^|_)door_FR$", re.I), "openDeg": 68},
254
+ {"id": "doorRL", "role": "door", "slot": re.compile(r"(?:^|_)door_RL$", re.I), "openDeg": -68},
255
+ {"id": "doorRR", "role": "door", "slot": re.compile(r"(?:^|_)door_RR$", re.I), "openDeg": 68},
256
+ {"id": "hood", "role": "bonnet", "slot": re.compile(r"(?:^|_)(?:hood|bonnet)(?:_(?:sedan|wagon))?$", re.I), "openDeg": 55},
257
+ {"id": "trunk", "role": "boot", "slot": re.compile(r"(?:^|_)(?:trunk|tailgate|boot)(?:_(?:sedan|wagon))?$", re.I), "openDeg": 55},
258
+ ]
259
+
260
+
261
+ def mesh_role(node: str) -> str:
262
+ lowered = node.lower()
263
+ return next((role for role, tokens in MESH_ROLE_RULES if any(token in lowered for token in tokens)), "body")
264
+
265
+
266
+ def canonical_jbeam_point(point: tuple[float, float, float]) -> list[float]:
267
+ """Map BeamNG +X-left/+Z-up/-Y-forward nodes into the HELIX frame."""
268
+ return [round(point[0], 6), round(point[2], 6), round(-point[1], 6)]
269
+
270
+
271
+ def break_groups(value: Any) -> list[str]:
272
+ if isinstance(value, str):
273
+ return [value] if value else []
274
+ if isinstance(value, list):
275
+ return [entry for entry in value if isinstance(entry, str) and entry]
276
+ return []
277
+
278
+
279
+ def source_hinge(body: dict[str, Any]) -> dict[str, Any] | None:
280
+ """Derive a rigid hinge only from explicitly hinge-tagged JBeam beams.
281
+
282
+ The two most distant panel-local hinge nodes define the axis. A lone node,
283
+ coincident nodes, malformed coordinates, or an origin pivot is insufficient
284
+ evidence and disables physical articulation for that panel.
285
+ """
286
+ positions: dict[str, tuple[float, float, float]] = {}
287
+ for row in decode_jbeam_table(body.get("nodes")):
288
+ node_id = row.get("id")
289
+ values = (row.get("posX"), row.get("posY"), row.get("posZ"))
290
+ if isinstance(node_id, str) and all(isinstance(value, (int, float)) and not isinstance(value, bool)
291
+ and math.isfinite(value) for value in values):
292
+ positions[node_id] = tuple(float(value) for value in values)
293
+ hinge_nodes: set[str] = set()
294
+ hinge_groups: set[str] = set()
295
+ for row in decode_jbeam_table(body.get("beams")):
296
+ groups = [group for group in break_groups(row.get("breakGroup")) if "hinge" in group.lower()]
297
+ if not groups:
298
+ continue
299
+ hinge_groups.update(groups)
300
+ for key in ("id1", "id2"):
301
+ node_id = row.get(key)
302
+ if isinstance(node_id, str) and node_id in positions:
303
+ hinge_nodes.add(node_id)
304
+ if len(hinge_nodes) < 2:
305
+ return None
306
+ pairs = []
307
+ for index, first_id in enumerate(sorted(hinge_nodes)):
308
+ for second_id in sorted(hinge_nodes)[index + 1:]:
309
+ first, second = positions[first_id], positions[second_id]
310
+ distance = math.dist(first, second)
311
+ pairs.append((distance, first_id, second_id, first, second))
312
+ distance, first_id, second_id, first, second = max(pairs, default=(0, "", "", (), ()))
313
+ if distance <= 1e-4:
314
+ return None
315
+ first_canonical = canonical_jbeam_point(first)
316
+ second_canonical = canonical_jbeam_point(second)
317
+ vector = [second_canonical[index] - first_canonical[index] for index in range(3)]
318
+ length = math.hypot(*vector)
319
+ if length <= 1e-4:
320
+ return None
321
+ axis = [round(value / length, 6) for value in vector]
322
+ pivot = [round((first_canonical[index] + second_canonical[index]) / 2, 6) for index in range(3)]
323
+ if math.hypot(*pivot) <= 1e-4:
324
+ return None
325
+ return {"pivot": pivot, "axis": axis, "nodeIds": [first_id, second_id],
326
+ "breakGroups": sorted(hinge_groups)}
327
+
328
+
329
+ def articulation_candidates(selected: list[tuple[str, str, dict[str, Any], Path]],
330
+ slot_rows: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
331
+ """Return runtime panels and source-only evidence, failing closed per panel."""
332
+ bodies = {part_name: body for _, part_name, body, _ in selected}
333
+ meshes = {part_name: sorted({str(row.get("mesh")) for row in decode_jbeam_table(body.get("flexbodies"))
334
+ if row.get("mesh")}) for part_name, body in bodies.items()}
335
+ descendants: dict[str, set[str]] = {}
336
+ for row in slot_rows:
337
+ host, child = row.get("hostPart"), row.get("selectedPart")
338
+ if row.get("resolution") == "resolved" and isinstance(host, str) and isinstance(child, str) \
339
+ and host in bodies and child in bodies and child:
340
+ descendants.setdefault(host, set()).add(child)
341
+
342
+ candidates: list[dict[str, Any]] = []
343
+ for rule in PANEL_RULES:
344
+ roots = sorted(name for name, body in bodies.items()
345
+ if isinstance(body.get("slotType"), str) and rule["slot"].search(body["slotType"]))
346
+ if len(roots) != 1:
347
+ continue
348
+ root = roots[0]
349
+ hinge = source_hinge(bodies[root])
350
+ if hinge is None:
351
+ continue
352
+ custody_parts: set[str] = set()
353
+ pending = [root]
354
+ while pending:
355
+ part_name = pending.pop()
356
+ if part_name in custody_parts:
357
+ continue
358
+ custody_parts.add(part_name)
359
+ pending.extend(sorted(descendants.get(part_name, set()), reverse=True))
360
+ nodes = sorted({node for part_name in custody_parts for node in meshes.get(part_name, [])})
361
+ if not nodes:
362
+ continue
363
+ panel = {"id": rule["id"], "role": rule["role"],
364
+ "signal": f"panel.{rule['id']}.openness", "nodes": nodes,
365
+ "hinge": {"pivot": hinge["pivot"], "axis": hinge["axis"],
366
+ "closedDeg": 0, "openDeg": rule["openDeg"]},
367
+ "collider": {"kind": "visual-bounds-box", "minHalfExtentM": .03},
368
+ "dynamics": {"detents": [0, .25, 1], "latchFraction": .05,
369
+ "latchAngularSpeedRadS": 1.5},
370
+ # Detachment is future eligibility only. No production fidelity
371
+ # tier may activate it until the runtime has an authoritative
372
+ # detached-body/network ownership contract.
373
+ "fidelity": {"physical": ["medium", "high", "ultra"], "detachable": []}}
374
+ candidates.append({"panel": panel, "rootPart": root,
375
+ "descendantParts": sorted(custody_parts - {root}),
376
+ "hingeNodeIds": hinge["nodeIds"], "hingeBreakGroups": hinge["breakGroups"]})
377
+
378
+ owners: dict[str, list[str]] = {}
379
+ for candidate in candidates:
380
+ for node in candidate["panel"]["nodes"]:
381
+ owners.setdefault(node, []).append(candidate["panel"]["id"])
382
+ ambiguous_panels = {panel_id for panel_ids in owners.values() if len(panel_ids) > 1 for panel_id in panel_ids}
383
+ accepted = [candidate for candidate in candidates if candidate["panel"]["id"] not in ambiguous_panels]
384
+ accepted.sort(key=lambda row: next(index for index, rule in enumerate(PANEL_RULES)
385
+ if rule["id"] == row["panel"]["id"]))
386
+ return [candidate["panel"] for candidate in accepted], accepted
387
+
388
+
389
+ def lamp_functions(part_name: str, channel: str) -> list[str]:
390
+ lowered = channel.lower()
391
+ rules: list[tuple[Callable[[], bool], list[str]]] = [
392
+ (lambda: "signal_l" in lowered, ["indicator_left"]),
393
+ (lambda: "signal_r" in lowered, ["indicator_right"]),
394
+ (lambda: "lowhighbrake" in lowered, ["tail", "brake"]),
395
+ (lambda: "brakelight" in lowered, ["brake"]),
396
+ (lambda: "reverse" in lowered, ["reverse"]),
397
+ (lambda: "fog" in lowered, ["fog_front"]),
398
+ (lambda: "highbeam" in lowered and "lowhigh" not in lowered, ["headlight_high"]),
399
+ (lambda: "lowhighbeam" in lowered,
400
+ ["tail" if any(token in part_name.lower() for token in ("tail", "trunklight")) else "headlight_low"]),
401
+ ]
402
+ return next((functions for predicate, functions in rules if predicate()), [])
403
+
404
+
405
+ def lamp_side(part_name: str, channel: str) -> str:
406
+ lowered_channel, lowered_part = channel.lower(), part_name.lower()
407
+ return next((side for side in ("_l", "_r")
408
+ if side in lowered_channel or lowered_part.endswith(side)), "")
409
+
410
+
411
+ LAMP_NODE_SEMANTICS = {
412
+ "reverse": ("reverse",), "fog_front": ("fog", "rally"),
413
+ "headlight_low": ("headlight",), "headlight_high": ("headlight",),
414
+ "indicator_left": ("signal",), "indicator_right": ("signal",),
415
+ "tail": ("taillight", "tail"), "brake": ("taillight", "tail"),
416
+ }
417
+
418
+ TRANSMISSION_DEVICE_KINDS = {
419
+ "manualgearbox": "manual",
420
+ "sequentialgearbox": "sequential",
421
+ "dctgearbox": "dct",
422
+ "cvtgearbox": "cvt",
423
+ "automaticgearbox": "automatic",
424
+ }
425
+
426
+ TRANSMISSION_INFO_KINDS = {
427
+ "manual": "manual",
428
+ "sequential": "sequential",
429
+ "dct": "dct",
430
+ "cvt": "cvt",
431
+ "automatic": "automatic",
432
+ }
433
+
434
+
435
+ def lamp_beam(light: str) -> dict[str, float] | None:
436
+ if light not in ("headlight_low", "headlight_high", "fog_front"):
437
+ return None
438
+ high = light == "headlight_high"
439
+ return {"halfAngleDeg": 16 if high else 28, "distance": 80 if high else 50,
440
+ "penumbra": .3, "intensity": 1}
441
+
442
+
443
+ def selected_powertrain_devices(selected: Iterable[tuple[str, str, dict[str, Any], Path]],
444
+ label_source: Callable[[Path], str]) -> list[dict[str, Any]]:
445
+ selected_parts = list(selected)
446
+ base_devices: list[tuple[str, str, Path, list[str], dict[str, Any]]] = []
447
+ for slot_name, part_name, body, path in selected_parts:
448
+ controllers = sorted(str(row["fileName"]) for row in decode_jbeam_table(body.get("controller"))
449
+ if isinstance(row.get("fileName"), str))
450
+ for row in decode_jbeam_table(body.get("powertrain")):
451
+ name = row.get("name")
452
+ if not isinstance(name, str) or not name:
453
+ continue
454
+ base_devices.append((slot_name, part_name, path, controllers, row))
455
+ device_names = {row[4]["name"] for row in base_devices}
456
+ overlay_index: dict[str, list[tuple[dict[str, Any], str]]] = {}
457
+ overlay_count = 0
458
+ for _, part_name, body, path in selected_parts:
459
+ for name, options in body.items():
460
+ if name not in device_names or not isinstance(options, dict):
461
+ continue
462
+ overlay_count += 1
463
+ if overlay_count > MAX_INDEX_PARTS:
464
+ fail(f"selected powertrain device overlays exceed {MAX_INDEX_PARTS}")
465
+ overlay_index.setdefault(name, []).append(
466
+ (options, f"{label_source(path)}#{part_name}.{name}"))
467
+
468
+ devices: list[dict[str, Any]] = []
469
+ for slot_name, part_name, path, controllers, row in base_devices:
470
+ name = row["name"]
471
+ merged = dict(row)
472
+ overlay_sources: list[str] = []
473
+ overlay_fields: dict[str, list[tuple[Any, str]]] = {}
474
+ for options, overlay_source in overlay_index.get(name, []):
475
+ overlay_sources.append(overlay_source)
476
+ for field, value in options.items():
477
+ overlay_fields.setdefault(field, []).append((value, overlay_source))
478
+ overlay_conflicts: list[dict[str, Any]] = []
479
+ for field, values in overlay_fields.items():
480
+ unique_values = {json.dumps(value, sort_keys=True, separators=(",", ":")) for value, _ in values}
481
+ if len(unique_values) == 1:
482
+ merged[field] = values[0][0]
483
+ else:
484
+ sources = [source for _, source in values]
485
+ merged[field] = {"ambiguousOverlaySources": sources}
486
+ overlay_conflicts.append({"field": field, "sources": sources})
487
+ devices.append({**merged, "partName": part_name, "slotType": slot_name,
488
+ "source": f"{label_source(path)}#{part_name}.powertrain",
489
+ "overlaySources": sorted(set(overlay_sources)),
490
+ "overlayFieldSources": {field: sorted({source for _, source in values})
491
+ for field, values in sorted(overlay_fields.items())},
492
+ "overlayConflicts": overlay_conflicts, "controllers": controllers})
493
+ return devices
494
+
495
+
496
+ def selected_transmission_kind(devices: Iterable[dict[str, Any]],
497
+ config_info: dict[str, Any]) -> tuple[str | None, str, str]:
498
+ """Return a kind only when the selected, fingerprinted source is unambiguous."""
499
+ evidence: list[tuple[str, str, str, str]] = []
500
+ for row in devices:
501
+ device_type = str(row.get("type", ""))
502
+ kind = TRANSMISSION_DEVICE_KINDS.get(device_type.lower())
503
+ if kind is not None:
504
+ evidence.append((kind, row["partName"], row["source"], device_type))
505
+ kinds = {row[0] for row in evidence}
506
+ info_value = str(config_info.get("Transmission", "")).strip()
507
+ info_kind = TRANSMISSION_INFO_KINDS.get(info_value.lower())
508
+ if len(kinds) > 1:
509
+ detail = ", ".join(sorted(f"{part}:{device}" for _, part, _, device in evidence))
510
+ return None, "selected JBeam#powertrain.type", f"Ambiguous selected transmission devices: {detail}."
511
+ if evidence:
512
+ kind, part_name, source, device_type = evidence[0]
513
+ if info_kind is not None and info_kind != kind:
514
+ return None, source, \
515
+ f"Selected {part_name} declares {device_type}, but configuration info declares {info_value}."
516
+ return kind, source, f"Selected {part_name} powertrain device type is {device_type}."
517
+ if info_kind is not None:
518
+ return info_kind, "selected configuration info#Transmission", \
519
+ f"No selected JBeam transmission device row was available; exact configuration info declares {info_value}."
520
+ return None, "selected JBeam#powertrain.type", \
521
+ "No recognized manualGearbox, sequentialGearbox, dctGearbox, cvtGearbox, or automaticGearbox device was selected."
522
+
523
+
524
+ def key_multimap(parts: Iterable[tuple[str, Any]], max_entries: int = 1_000_000,
525
+ max_depth: int = 128) -> dict[str, list[tuple[str, Any]]]:
526
+ """Index keys once with pending memory proportional to nesting depth."""
527
+ result: dict[str, list[tuple[str, Any]]] = {}
528
+ # Each frame owns an iterator and advances only one child before descending,
529
+ # so a million-wide object never becomes a million-entry pending stack.
530
+ stack: list[tuple[str, str | None, Any, int]] = [("roots", None, iter(parts), -1)]
531
+ entries = 0
532
+ while stack:
533
+ kind, part, iterator, parent_depth = stack[-1]
534
+ try:
535
+ item = next(iterator)
536
+ except StopIteration:
537
+ stack.pop()
538
+ continue
539
+ if kind == "roots":
540
+ part, node = item
541
+ depth = 0
542
+ elif kind == "dict":
543
+ name, node = item
544
+ normalized = str(name).lstrip("$+")
545
+ if normalized in WATCHED_KEYS:
546
+ result.setdefault(normalized, []).append((str(part), node))
547
+ depth = parent_depth + 1
548
+ else:
549
+ node = item
550
+ depth = parent_depth + 1
551
+ if depth > max_depth:
552
+ fail(f"selected JBeam closure exceeds nested depth cap {max_depth}")
553
+ entries += 1
554
+ if entries > max_entries:
555
+ fail(f"selected JBeam closure exceeds traversal cap {max_entries}")
556
+ if isinstance(node, dict):
557
+ stack.append(("dict", str(part), iter(node.items()), depth))
558
+ elif isinstance(node, list):
559
+ stack.append(("list", str(part), iter(node), depth))
560
+ return result
561
+
562
+
563
+ def indexed_values(index: dict[str, list[tuple[str, Any]]], key: str,
564
+ part_filter: Callable[[str], bool] | None = None) -> list[Any]:
565
+ return [value for part, value in index.get(key.lstrip("$+"), []) if part_filter is None or part_filter(part)]
566
+
567
+
568
+ def number(index: dict[str, list[tuple[str, Any]]], keys: Iterable[str], default: float,
569
+ part_filter: Callable[[str], bool] | None = None) -> tuple[float, str]:
570
+ for key in keys:
571
+ values = indexed_values(index, key, part_filter)
572
+ for value in reversed(values):
573
+ if literal(value) and isinstance(value, (int, float)):
574
+ return float(value), key
575
+ return default, "HELIX-default"
576
+
577
+
578
+ def sourced(value: Any, unit: str, source: str | None = None, note: str | None = None) -> dict[str, Any]:
579
+ row: dict[str, Any] = {"value": value, "unit": unit}
580
+ if source:
581
+ row["sources"] = [source]
582
+ else:
583
+ row["derivedFrom"] = note or "Explicit HELIX runtime prior; BeamNG source has no directly equivalent literal."
584
+ if note:
585
+ row["note"] = note
586
+ return row
587
+
588
+
589
+ SUSPENSION_AXLE_PATTERN = re.compile(r"(?:^|_)(?:strut|coilover)_?([FR])(?:_|$)", re.I)
590
+ SWAYBAR_AXLE_PATTERN = re.compile(r"(?:^|_)(?:rally_)?swaybar_?([FR])(?:_|$)", re.I)
591
+ TIRE_AXLE_PATTERN = re.compile(r"(?:^|_)tire_?([FR])(?:_|$)", re.I)
592
+
593
+
594
+ def selected_variable_environment(selected: Iterable[tuple[str, str, dict[str, Any], Path]],
595
+ config: dict[str, Any], config_path: Path,
596
+ label_source: Callable[[Path], str]) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]:
597
+ """Resolve the selected JBeam defaults, then exact `.pc` variable overrides."""
598
+ values: dict[str, Any] = {}
599
+ receipts: dict[str, dict[str, Any]] = {}
600
+ for _, part_name, body, path in selected:
601
+ for row in decode_jbeam_table(body.get("variables")):
602
+ name, default = row.get("name"), row.get("default")
603
+ if not isinstance(name, str) or not re.fullmatch(r"\$[A-Za-z_][A-Za-z0-9_]*", name):
604
+ continue
605
+ if isinstance(default, float) and not math.isfinite(default):
606
+ continue
607
+ values[name] = default
608
+ receipts[name] = {"value": default, "selection": "selected-default",
609
+ "source": f"{label_source(path)}#{part_name}.variables.{name}.default"}
610
+ overrides = config.get("vars")
611
+ if isinstance(overrides, dict):
612
+ for name in sorted(overrides):
613
+ value = overrides[name]
614
+ if not isinstance(name, str) or not re.fullmatch(r"\$[A-Za-z_][A-Za-z0-9_]*", name):
615
+ continue
616
+ # Keep unsupported scalar/object overrides in the environment so
617
+ # they fail the exact expression rather than silently falling back
618
+ # to the JBeam default they override.
619
+ values[name] = value
620
+ receipt = {"value": value, "selection": "configuration-override",
621
+ "source": f"{config_path.name}#vars.{name}"}
622
+ if isinstance(value, float) and not math.isfinite(value):
623
+ # JSON has no portable non-finite number representation. Keep
624
+ # the evaluator's exact value, but serialize an explicit safe
625
+ # receipt instead of emitting invalid terminal JSON.
626
+ receipt.update({"value": None, "raw": repr(value), "status": "unsupported"})
627
+ receipts[name] = receipt
628
+ return values, receipts
629
+
630
+
631
+ def numeric_resolution(value: Any, variables: dict[str, Any]) -> ExpressionResolution:
632
+ result = resolve_beamng_expression(value, variables)
633
+ if result.status != "unsupported" and (isinstance(result.value, bool)
634
+ or not isinstance(result.value, (int, float))
635
+ or not math.isfinite(result.value)):
636
+ return ExpressionResolution("unsupported", None, result.variables,
637
+ "expression did not resolve to a finite number")
638
+ if result.status != "unsupported":
639
+ return ExpressionResolution(result.status, round(float(result.value), 12),
640
+ result.variables, result.reason)
641
+ return result
642
+
643
+
644
+ def lower_selected_brakes(selected: list[tuple[str, str, dict[str, Any], Path]],
645
+ variables: dict[str, Any], variable_receipts: dict[str, dict[str, Any]],
646
+ label_source: Callable[[Path], str], citation_id: str,
647
+ provenance: list[dict[str, Any]]) -> tuple[dict[str, Any], dict[str, Any], bool]:
648
+ """Lower the two selected brake-part option blocks into HELIX totals.
649
+
650
+ BeamNG's torque values apply to each wheel in the part-owned axle section;
651
+ HELIX's values are totals across the car. Front bias is therefore derived
652
+ from the per-wheel front/rear values before both axles are doubled.
653
+ """
654
+ candidates: dict[str, list[dict[str, Any]]] = {"front": [], "rear": []}
655
+ unowned: list[dict[str, Any]] = []
656
+ for slot_name, part_name, body, path in selected:
657
+ options = pressure_wheel_options(body)
658
+ if "brakeTorque" not in options and "parkingTorque" not in options:
659
+ continue
660
+ axle = selected_part_axle(BRAKE_AXLE_PATTERN, slot_name, part_name, body)
661
+ receipt = {"slotType": slot_name, "partName": part_name,
662
+ "source": f"{label_source(path)}#{part_name}.pressureWheels",
663
+ "options": {key: options[key] for key in ("brakeTorque", "brakeInputSplit",
664
+ "brakeSplitCoef", "parkingTorque")
665
+ if key in options}}
666
+ if axle is None:
667
+ # Suspension defaults deliberately clear inherited brake options;
668
+ # they do not own an axle brake Package and cannot override the
669
+ # exact selected brake part.
670
+ if "brake" in slot_name.lower() or "brake" in part_name.lower():
671
+ unowned.append(receipt)
672
+ continue
673
+ candidates[axle].append(receipt)
674
+
675
+ axles: dict[str, dict[str, Any]] = {}
676
+ complete = True
677
+ for axle in ("front", "rear"):
678
+ rows = candidates[axle]
679
+ if len(rows) != 1:
680
+ complete = False
681
+ detail = f"Expected one selected {axle} brake part; found {len(rows)}."
682
+ provenance.append({"target": f"physics.brakes.{axle}",
683
+ "source": "selected JBeam pressureWheels",
684
+ "disposition": "unsupported", "detail": detail})
685
+ axles[axle] = {"status": "unsupported", "reason": detail,
686
+ "candidates": rows}
687
+ continue
688
+ receipt = rows[0]
689
+ options = receipt["options"]
690
+ fields: dict[str, Any] = {}
691
+ for field in ("brakeTorque", "parkingTorque"):
692
+ result = numeric_resolution(options.get(field), variables)
693
+ disposition = "source-literal" if result.status == "literal" else result.status
694
+ row: dict[str, Any] = {"raw": options.get(field), "status": result.status,
695
+ "variables": list(result.variables)}
696
+ if result.status != "unsupported":
697
+ row["value"] = result.value
698
+ else:
699
+ row["reason"] = result.reason
700
+ complete = False
701
+ fields[field] = row
702
+ provenance.append({"target": f"sourceSemantics.brakes.axles.{axle}.{field}",
703
+ "source": receipt["source"], "disposition": disposition,
704
+ "detail": result.reason or
705
+ (f"Selected literal {result.value:g}." if result.status == "literal" else
706
+ f"Resolved {options.get(field)!r} to {result.value:g} from selected variables.")})
707
+ for field in ("brakeInputSplit", "brakeSplitCoef"):
708
+ if field in options:
709
+ fields[field] = {"raw": options[field], "status": "preserved-source"}
710
+ axle_row = {key: value for key, value in receipt.items() if key != "options"}
711
+ axle_row.update(fields)
712
+ axles[axle] = axle_row
713
+
714
+ front_torque = axles.get("front", {}).get("brakeTorque", {}).get("value")
715
+ rear_torque = axles.get("rear", {}).get("brakeTorque", {}).get("value")
716
+ front_parking = axles.get("front", {}).get("parkingTorque", {}).get("value")
717
+ rear_parking = axles.get("rear", {}).get("parkingTorque", {}).get("value")
718
+ complete = complete and all(isinstance(value, (int, float)) and not isinstance(value, bool)
719
+ for value in (front_torque, rear_torque, front_parking, rear_parking))
720
+ if complete:
721
+ complete = bool(front_torque > 0 and rear_torque > 0
722
+ and front_parking >= 0 and rear_parking >= 0
723
+ and front_parking + rear_parking > 0)
724
+ if not complete:
725
+ provenance.append({"target": "physics.brakes", "source": "selected JBeam pressureWheels",
726
+ "disposition": "unsupported",
727
+ "detail": "Selected brake torque must be positive on both axles and parking torque must be non-negative and present on at least one axle."})
728
+
729
+ if complete:
730
+ per_wheel_total = float(front_torque + rear_torque)
731
+ max_torque = round(per_wheel_total * 2, 6)
732
+ bias_front = round(float(front_torque) / per_wheel_total, 8)
733
+ parking_by_axle = {"front": float(front_parking), "rear": float(rear_parking)}
734
+ active_parking = [axle for axle, torque in parking_by_axle.items() if torque > 0]
735
+ handbrake_axle = active_parking[0] if len(active_parking) == 1 else "all"
736
+ handbrake_torque = round(sum(parking_by_axle.values()) * 2, 6)
737
+ brakes = {
738
+ "maxTorque": sourced(max_torque, "N.m", citation_id,
739
+ "Twice the sum of selected per-wheel front and rear brake torque."),
740
+ "biasFront": sourced(bias_front, "ratio", citation_id,
741
+ "Selected front per-wheel torque divided by front plus rear per-wheel torque."),
742
+ "handbrakeTorque": sourced(handbrake_torque, "N.m", citation_id,
743
+ "Twice the selected per-wheel parking torque across its active axle(s)."),
744
+ "handbrakeAxle": handbrake_axle,
745
+ }
746
+ for target, detail in (
747
+ ("physics.brakes.maxTorque", f"2 * ({front_torque:g} + {rear_torque:g}) = {max_torque:g} N.m."),
748
+ ("physics.brakes.biasFront", f"{front_torque:g} / ({front_torque:g} + {rear_torque:g}) = {bias_front:g}."),
749
+ ("physics.brakes.handbrakeTorque", f"2 * ({front_parking:g} + {rear_parking:g}) = {handbrake_torque:g} N.m on {handbrake_axle}."),
750
+ ):
751
+ provenance.append({"target": target, "source": "selected JBeam brake-part pressureWheels rows",
752
+ "disposition": "derived", "detail": detail})
753
+ else:
754
+ brakes = {
755
+ "maxTorque": sourced(3900, "N.m", note="HELIX functional fallback; selected brake rows were incomplete or unsupported."),
756
+ "biasFront": sourced(.6923, "ratio", note="HELIX functional fallback; selected axle torque was incomplete or unsupported."),
757
+ "handbrakeTorque": sourced(1800, "N.m", note="HELIX functional fallback; selected parking torque was incomplete or unsupported."),
758
+ "handbrakeAxle": "rear",
759
+ }
760
+ provenance.append({"target": "physics.brakes", "source": "HELIX functional fallback",
761
+ "disposition": "defaulted",
762
+ "detail": "Selected front/rear brake and parking torque rows did not all resolve; brake domain remains non-functional."})
763
+
764
+ used_variables = sorted({name for row in axles.values() if isinstance(row, dict)
765
+ for field in ("brakeTorque", "parkingTorque")
766
+ for name in row.get(field, {}).get("variables", [])})
767
+ semantics = {"variables": {name: variable_receipts[name] for name in used_variables
768
+ if name in variable_receipts},
769
+ "axles": axles, "unownedPressureWheelOptions": sorted(
770
+ unowned, key=lambda row: (row["slotType"], row["partName"], row["source"]))}
771
+ return brakes, semantics, complete
772
+
773
+
774
+ def suspension_row_side(row: dict[str, Any]) -> str | None:
775
+ labels = [row.get("name"), row.get("id1"), row.get("id2")]
776
+ sides: set[str] = set()
777
+ for label in labels:
778
+ if not isinstance(label, str):
779
+ continue
780
+ match = re.search(r"(?:_|[0-9])([LR])$", label, re.I)
781
+ if match:
782
+ sides.add("left" if match.group(1).lower() == "l" else "right")
783
+ return next(iter(sides)) if len(sides) == 1 else None
784
+
785
+
786
+ def resolution_receipt(raw: Any, variables: dict[str, Any], unit: str,
787
+ source: str, provenance: list[dict[str, Any]], target: str) -> dict[str, Any]:
788
+ row, _ = numeric_receipt(raw, variables, unit)
789
+ disposition = "source-literal" if row["status"] == "literal" else row["status"]
790
+ detail = row.get("reason") or (f"Selected literal {row['value']:g} {unit}."
791
+ if row["status"] == "literal" else
792
+ f"Resolved {raw!r} to {row['value']:g} {unit} from selected variables.")
793
+ provenance.append({"target": target, "source": source,
794
+ "disposition": disposition, "detail": detail})
795
+ return row
796
+
797
+
798
+ def symmetric_suspension_field(rows: list[dict[str, Any]], field: str,
799
+ variables: dict[str, Any], unit: str, source: str,
800
+ provenance: list[dict[str, Any]], target: str,
801
+ positive: bool = False,
802
+ require_velocity_split: bool = False) -> dict[str, Any]:
803
+ """Collapse only a matching left/right pair from one selected leaf part."""
804
+ by_side: dict[str, list[tuple[Any, ExpressionResolution, dict[str, Any]]]] = {
805
+ "left": [], "right": []}
806
+ unsupported: list[ExpressionResolution] = []
807
+ unsupported_variables: set[str] = set()
808
+ for row in rows:
809
+ if field not in row:
810
+ continue
811
+ raw = row[field]
812
+ result = numeric_resolution(raw, variables)
813
+ if result.status == "unsupported":
814
+ unsupported.append(result)
815
+ unsupported_variables.update(result.variables)
816
+ continue
817
+ if positive and not (result.value > 0):
818
+ continue
819
+ side = suspension_row_side(row)
820
+ if side:
821
+ by_side[side].append((raw, result, row))
822
+ reason = None
823
+ chosen: list[tuple[Any, ExpressionResolution, dict[str, Any]]] = []
824
+ if unsupported:
825
+ reason = unsupported[0].reason
826
+ else:
827
+ for side in ("left", "right"):
828
+ values = by_side[side]
829
+ if len(values) != 1:
830
+ reason = f"expected one selected {side} {field} row; found {len(values)}"
831
+ break
832
+ chosen.append(values[0])
833
+ if reason is None and chosen[0][1].value != chosen[1][1].value:
834
+ reason = f"selected left/right {field} values do not match"
835
+ velocity_split = None
836
+ if reason is None and require_velocity_split:
837
+ split_rows = []
838
+ for _, _, row in chosen:
839
+ if "beamDampVelocitySplit" not in row:
840
+ reason = f"selected {field} rows are missing beamDampVelocitySplit"
841
+ break
842
+ split_rows.append((row["beamDampVelocitySplit"],
843
+ numeric_resolution(row["beamDampVelocitySplit"], variables)))
844
+ if reason is None:
845
+ unsupported_split = next((result for _, result in split_rows
846
+ if result.status == "unsupported"), None)
847
+ if unsupported_split:
848
+ unsupported_variables.update(unsupported_split.variables)
849
+ reason = unsupported_split.reason
850
+ elif split_rows[0][1].value != split_rows[1][1].value:
851
+ reason = f"selected left/right {field} beamDampVelocitySplit values do not match"
852
+ elif not isinstance(split_rows[0][1].value, (int, float)):
853
+ reason = f"selected {field} beamDampVelocitySplit is not numeric"
854
+ elif split_rows[0][1].value < 0:
855
+ reason = f"selected {field} beamDampVelocitySplit must be non-negative"
856
+ else:
857
+ velocity_split = split_rows[0]
858
+ if reason is not None:
859
+ receipt = {"status": "unsupported", "unit": unit, "reason": reason,
860
+ "variables": sorted(unsupported_variables)}
861
+ provenance.append({"target": target, "source": source,
862
+ "disposition": "unsupported", "detail": reason})
863
+ return receipt
864
+ raw = chosen[0][0]
865
+ result = chosen[0][1]
866
+ receipt = {"raw": raw, "status": result.status, "value": result.value,
867
+ "unit": unit, "variables": list(result.variables), "symmetricRows": 2}
868
+ if velocity_split is not None:
869
+ split_raw, split_result = velocity_split
870
+ receipt["velocitySplit"] = {
871
+ "raw": split_raw, "status": split_result.status, "value": split_result.value,
872
+ "unit": "m/s", "variables": list(split_result.variables), "symmetricRows": 2,
873
+ }
874
+ receipt["variables"] = sorted(set(receipt["variables"]) | set(split_result.variables))
875
+ disposition = "source-literal" if result.status == "literal" else "derived"
876
+ provenance.append({"target": target, "source": source, "disposition": disposition,
877
+ "detail": (f"Matching selected left/right rows resolve to {result.value:g} {unit}.")})
878
+ return receipt
879
+
880
+
881
+ def lower_selected_suspension(selected: list[tuple[str, str, dict[str, Any], Path]],
882
+ variables: dict[str, Any], variable_receipts: dict[str, dict[str, Any]],
883
+ label_source: Callable[[Path], str],
884
+ chassis_mass: float, weight_front: float,
885
+ provenance: list[dict[str, Any]]) -> tuple[dict[str, Any], dict[str, Any]]:
886
+ """Extract selected strut/coilover semantics without treating structure as suspension.
887
+
888
+ BeamNG authors spring and velocity-sensitive damper beams at the selected
889
+ strut. HELIX currently authors wheel rate plus one global slow damper
890
+ triplet. The exact source values and their beam-space derivations are
891
+ retained here, while the runtime projection remains fail-closed until a
892
+ motion ratio and axle-specific fast/slow damping contract are available.
893
+ """
894
+ leaves: dict[str, list[tuple[str, str, dict[str, Any], Path]]] = {"front": [], "rear": []}
895
+ excluded: list[dict[str, Any]] = []
896
+ anti_roll_candidates: dict[str, list[tuple[str, str, dict[str, Any], Path]]] = {
897
+ "front": [], "rear": []}
898
+ for slot_name, part_name, body, path in selected:
899
+ axle = selected_part_axle(SUSPENSION_AXLE_PATTERN, slot_name, part_name, body)
900
+ if axle:
901
+ leaves[axle].append((slot_name, part_name, body, path))
902
+ continue
903
+ sway_axle = selected_part_axle(SWAYBAR_AXLE_PATTERN, slot_name, part_name, body)
904
+ if sway_axle:
905
+ anti_roll_candidates[sway_axle].append((slot_name, part_name, body, path))
906
+ label = " ".join(str(value) for value in (slot_name, body.get("slotType"), part_name)).lower()
907
+ if not any(token in label for token in ("suspension", "strut", "swaybar")):
908
+ continue
909
+ structural_rows = [row for row in decode_jbeam_table(body.get("beams"))
910
+ if "beamSpring" in row or "beamDamp" in row]
911
+ if structural_rows:
912
+ reason = ("swaybar construction beams do not own anti-roll stiffness" if sway_axle else
913
+ "not selected through a strut/coilover leaf slot")
914
+ excluded.append({"slotType": slot_name, "partName": part_name,
915
+ "source": f"{label_source(path)}#{part_name}.beams",
916
+ "beamRows": len(structural_rows), "reason": reason})
917
+
918
+ axles: dict[str, dict[str, Any]] = {}
919
+ used_variables: set[str] = set()
920
+ extraction_complete = True
921
+ for axle in ("front", "rear"):
922
+ candidates = leaves[axle]
923
+ if len(candidates) != 1:
924
+ reason = f"Expected one selected {axle} strut/coilover leaf part; found {len(candidates)}."
925
+ provenance.append({"target": f"sourceSemantics.suspension.axles.{axle}",
926
+ "source": "selected JBeam part custody",
927
+ "disposition": "unsupported", "detail": reason})
928
+ axles[axle] = {"status": "unsupported", "reason": reason,
929
+ "candidates": [part for _, part, _, _ in candidates]}
930
+ extraction_complete = False
931
+ continue
932
+ slot_name, part_name, body, path = candidates[0]
933
+ source = f"{label_source(path)}#{part_name}.beams"
934
+ beams = decode_jbeam_table(body.get("beams"))
935
+ variable_springs = [row for row in beams if isinstance(row.get("beamSpring"), str)
936
+ and "precompressionRange" in row
937
+ and row["beamSpring"].startswith("$")]
938
+ literal_springs = [] if variable_springs else [
939
+ row for row in beams if isinstance(row.get("beamSpring"), (int, float))
940
+ and not isinstance(row.get("beamSpring"), bool)
941
+ and row["beamSpring"] > 0 and row.get("beamDamp") == 0
942
+ and "precompressionRange" in row]
943
+ spring_rows = variable_springs or literal_springs
944
+ spring = symmetric_suspension_field(
945
+ spring_rows, "beamSpring", variables, "N/m", source, provenance,
946
+ f"sourceSemantics.suspension.axles.{axle}.springRate", positive=True)
947
+ precompression = symmetric_suspension_field(
948
+ spring_rows, "precompressionRange", variables, "m", source, provenance,
949
+ f"sourceSemantics.suspension.axles.{axle}.precompressionRange")
950
+ damper_rows = [row for row in beams if row.get("beamSpring") == 0
951
+ and ("beamDampVelocitySplit" in row
952
+ or re.match(r"^(?:shock|damper)_", str(row.get("name", "")), re.I))]
953
+ damping = {}
954
+ for output_name, field in (("slowBump", "beamDamp"), ("fastBump", "beamDampFast"),
955
+ ("slowRebound", "beamDampRebound"),
956
+ ("fastRebound", "beamDampReboundFast")):
957
+ damping[output_name] = symmetric_suspension_field(
958
+ damper_rows, field, variables, "N.s/m", source, provenance,
959
+ f"sourceSemantics.suspension.axles.{axle}.damping.{output_name}",
960
+ positive=True, require_velocity_split=True)
961
+ travel_rows = [row for row in beams if row.get("beamSpring") == 0 and row.get("beamDamp") == 0
962
+ and not row.get("name") and "beamDampVelocitySplit" not in row
963
+ and isinstance(row.get("shortBoundRange"), (int, float))
964
+ and 0 < row["shortBoundRange"] <= .5]
965
+ bump_limit = symmetric_suspension_field(
966
+ travel_rows, "shortBoundRange", variables, "m", source, provenance,
967
+ f"sourceSemantics.suspension.axles.{axle}.bumpLimit", positive=True)
968
+ required = [spring, precompression, bump_limit, *damping.values()]
969
+ axle_complete = not any(row.get("status") == "unsupported" for row in required)
970
+ if not axle_complete:
971
+ extraction_complete = False
972
+ for row in required:
973
+ used_variables.update(row.get("variables", []))
974
+ derived: dict[str, Any] = {}
975
+ if spring.get("status") != "unsupported":
976
+ corner_mass = chassis_mass * (weight_front if axle == "front" else 1 - weight_front) / 2
977
+ critical = 2 * math.sqrt(float(spring["value"]) * corner_mass)
978
+ derived = {
979
+ "cornerMassKg": round(corner_mass, 6),
980
+ "beamNaturalHz": round(math.sqrt(float(spring["value"]) / corner_mass) / (2 * math.pi), 8),
981
+ "criticalDampingNsPerM": round(critical, 6),
982
+ }
983
+ for output_name, ratio_name in (("slowBump", "slowBumpRatio"),
984
+ ("fastBump", "fastBumpRatio"),
985
+ ("slowRebound", "slowReboundRatio"),
986
+ ("fastRebound", "fastReboundRatio")):
987
+ value = damping[output_name].get("value")
988
+ if isinstance(value, (int, float)):
989
+ derived[ratio_name] = round(float(value) / critical, 8)
990
+ provenance.append({"target": f"sourceSemantics.suspension.axles.{axle}.derived",
991
+ "source": source, "disposition": "derived",
992
+ "detail": "Beam natural frequency and damping ratios derived from selected beam rates and the emitted HELIX corner mass; not claimed as wheel-space ride values without motion ratio."})
993
+ axles[axle] = {"status": "resolved" if axle_complete else "unsupported",
994
+ "slotType": slot_name, "partName": part_name, "source": source,
995
+ "springRate": spring, "precompressionRange": precompression,
996
+ "damping": damping, "bumpLimit": bump_limit, "derived": derived,
997
+ "rowCounts": {"spring": len(spring_rows), "damper": len(damper_rows),
998
+ "travelLimit": len(travel_rows)}}
999
+
1000
+ anti_roll: dict[str, Any] = {}
1001
+ for axle in ("front", "rear"):
1002
+ candidates = anti_roll_candidates[axle]
1003
+ if len(candidates) != 1:
1004
+ anti_roll[axle] = {"status": "unsupported",
1005
+ "reason": f"expected one selected {axle} swaybar part; found {len(candidates)}"}
1006
+ extraction_complete = False
1007
+ continue
1008
+ slot_name, part_name, body, path = candidates[0]
1009
+ source = f"{label_source(path)}#{part_name}.torsionbars"
1010
+ torsion_rows = [row for row in decode_jbeam_table(body.get("torsionbars")) if "spring" in row]
1011
+ if len(torsion_rows) == 1:
1012
+ torsion = resolution_receipt(torsion_rows[0]["spring"], variables, "N.m/rad", source,
1013
+ provenance, f"sourceSemantics.suspension.antiRoll.{axle}.torsionSpring")
1014
+ used_variables.update(torsion.get("variables", []))
1015
+ else:
1016
+ reason = f"expected one selected torsionbar spring row; found {len(torsion_rows)}"
1017
+ torsion = {"status": "unsupported", "unit": "N.m/rad", "reason": reason,
1018
+ "variables": []}
1019
+ provenance.append({"target": f"sourceSemantics.suspension.antiRoll.{axle}.torsionSpring",
1020
+ "source": source, "disposition": "unsupported", "detail": reason})
1021
+ if torsion.get("status") == "unsupported":
1022
+ extraction_complete = False
1023
+ anti_roll[axle] = {"slotType": slot_name, "partName": part_name, "source": source,
1024
+ "torsionSpring": torsion, "runtimeStatus": "unsupported",
1025
+ "reason": "BeamNG torsional stiffness requires selected lever-arm and wheel motion-ratio geometry before HELIX N/m conversion."}
1026
+
1027
+ runtime_blockers = [
1028
+ "Selected beam spring rate requires wheel motion ratio before it is a HELIX wheel-space ride frequency.",
1029
+ "HELIX PackageSuspension has one global damping triplet and no axle-specific fast/slow velocity split.",
1030
+ "BeamNG strut bounds and precompression require suspension motion geometry before HELIX static ride height/travel conversion.",
1031
+ "BeamNG torsionbar stiffness requires lever-arm and wheel motion-ratio geometry before HELIX anti-roll N/m conversion.",
1032
+ ]
1033
+ for target, detail in (("rideHzFront", runtime_blockers[0]), ("rideHzRear", runtime_blockers[0]),
1034
+ ("dampingRatio", runtime_blockers[1]), ("bumpScale", runtime_blockers[1]),
1035
+ ("reboundScale", runtime_blockers[1]), ("staticRideHeight", runtime_blockers[2]),
1036
+ ("bumpTravel", runtime_blockers[2]), ("antiRollBarFront", runtime_blockers[3]),
1037
+ ("antiRollBarRear", runtime_blockers[3])):
1038
+ provenance.append({"target": f"physics.suspension.{target}",
1039
+ "source": "selected JBeam suspension semantics",
1040
+ "disposition": "unsupported", "detail": detail})
1041
+ suspension = {
1042
+ "rideHzFront": sourced(1.8, "Hz", note="HELIX sports prior; selected beam rate lacks wheel motion ratio."),
1043
+ "rideHzRear": sourced(1.8, "Hz", note="HELIX sports prior; selected beam rate lacks wheel motion ratio."),
1044
+ "dampingRatio": sourced(.45, "ratio", note="HELIX sports prior; selected axle fast/slow damping cannot fit the global runtime field exactly."),
1045
+ "bumpScale": sourced(1, "ratio", note="HELIX solver prior; selected axle damping is retained in source semantics."),
1046
+ "reboundScale": sourced(1, "ratio", note="HELIX solver prior; selected axle damping is retained in source semantics."),
1047
+ "staticRideHeight": sourced(.34, "m", note="Wheel-radius-derived prior; selected strut geometry does not yet provide wheel-space ride height."),
1048
+ "bumpTravel": sourced(.1, "m", note="HELIX sports prior; selected strut bound is retained in beam space."),
1049
+ "antiRollBarFront": sourced(0, "N/m", note="HELIX off fallback; selected torsionbar lacks exact wheel-space conversion."),
1050
+ "antiRollBarRear": sourced(0, "N/m", note="HELIX off fallback; selected torsionbar lacks exact wheel-space conversion."),
1051
+ }
1052
+ provenance.append({"target": "physics.suspension", "source": "HELIX functional fallback",
1053
+ "disposition": "defaulted",
1054
+ "detail": "Exact selected leaf semantics were retained, but runtime suspension remains non-functional until every wheel-space blocker is resolved."})
1055
+ counts = {"selectedLeafParts": sum(len(rows) for rows in leaves.values()),
1056
+ "excludedStructuralParts": len(excluded),
1057
+ "resolvedAxles": sum(row.get("status") != "unsupported" for row in axles.values()),
1058
+ "sourceRows": sum(sum(row.get("rowCounts", {}).values()) for row in axles.values())}
1059
+ semantics = {"variables": {name: variable_receipts[name] for name in sorted(used_variables)
1060
+ if name in variable_receipts},
1061
+ "axles": axles, "antiRoll": anti_roll,
1062
+ "excludedStructuralParts": sorted(excluded, key=lambda row: (row["slotType"], row["partName"])),
1063
+ "runtimeBlockers": runtime_blockers, "extractionComplete": extraction_complete,
1064
+ "counts": counts}
1065
+ return suspension, semantics
1066
+
1067
+
1068
+ def diff(kind: str, ratio: float, *, preload: float = 0, lock: float = 0, reverse: float = 0,
1069
+ torque_split: float = .5, friction: float = 0, dynamic: float = 0,
1070
+ lock_torque: float = 0, viscous_coef: float = 0,
1071
+ viscous_torque: float = 0) -> dict[str, Any]:
1072
+ return {"type": kind, "torqueSplit": torque_split, "gearRatio": ratio, "lockTorque": lock_torque,
1073
+ "viscousCoef": viscous_coef, "viscousTorque": viscous_torque, "lsdPreload": preload,
1074
+ "lsdLockCoef": lock, "lsdRevLockCoef": reverse, "friction": friction, "dynamicFriction": dynamic}
1075
+
1076
+
1077
+ def numeric_receipt(raw: Any, variables: dict[str, Any], unit: str,
1078
+ minimum: float | None = None, positive: bool = False) -> tuple[dict[str, Any], bool]:
1079
+ result = numeric_resolution(raw, variables)
1080
+ row: dict[str, Any] = {"raw": raw, "status": result.status, "unit": unit,
1081
+ "variables": list(result.variables)}
1082
+ valid = result.status != "unsupported"
1083
+ if valid:
1084
+ value = float(result.value)
1085
+ row["value"] = value
1086
+ if (positive and value <= 0) or (minimum is not None and value < minimum):
1087
+ row["status"] = "unsupported"
1088
+ row["reason"] = "resolved value is outside the admitted runtime range"
1089
+ valid = False
1090
+ else:
1091
+ row["reason"] = result.reason
1092
+ return row, valid
1093
+
1094
+
1095
+ MU_LUT_POINTS = 24
1096
+ MU_LUT_FZ = [round(math.exp(math.log(100) + ((math.log(40000) - math.log(100)) * index)
1097
+ / (MU_LUT_POINTS - 1)), 9) for index in range(MU_LUT_POINTS)]
1098
+ TYRE_SHAPE_PRIOR = {"alphaPeakRad": .139626, "cxMult": 1.05, "sigmaX": .048,
1099
+ "sigmaY": .08, "camberGain": .15, "dxOverDy": 1, "sFall": .3,
1100
+ "rollingResistance": .013, "angularInertia": 1.3}
1101
+
1102
+
1103
+ def lower_selected_tyres(selected: list[tuple[str, str, dict[str, Any], Path]],
1104
+ variables: dict[str, Any], variable_receipts: dict[str, dict[str, Any]],
1105
+ label_source: Callable[[Path], str],
1106
+ provenance: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, Any], bool]:
1107
+ """Lower one exact selected pressure-wheel tire Package per axle.
1108
+
1109
+ BeamNG supplies load-sensitivity coefficients while HELIX consumes its
1110
+ engine-wide exponential kernel as a compact LUT. Sampling uses the same
1111
+ fixed 24-knot logarithmic grid as the engine; it is not a per-vehicle fit.
1112
+ """
1113
+ candidates: dict[str, list[dict[str, Any]]] = {"front": [], "rear": []}
1114
+ for slot_name, part_name, body, path in selected:
1115
+ options = pressure_wheel_options(body)
1116
+ if not any(key in options for key in ("pressurePSI", "frictionCoef", "noLoadCoef")):
1117
+ continue
1118
+ axle = selected_part_axle(TIRE_AXLE_PATTERN, slot_name, part_name, body)
1119
+ if axle is not None:
1120
+ candidates[axle].append({"slotType": slot_name, "partName": part_name,
1121
+ "source": f"{label_source(path)}#{part_name}.pressureWheels",
1122
+ "options": options})
1123
+
1124
+ field_specs = {
1125
+ "pressurePSI": ("psi", 0.0, False), "radius": ("m", None, True),
1126
+ "tireWidth": ("m", None, True), "frictionCoef": ("ratio", None, True),
1127
+ "slidingFrictionCoef": ("ratio", None, True), "noLoadCoef": ("ratio", None, True),
1128
+ "fullLoadCoef": ("ratio", None, True), "loadSensitivitySlope": ("1/N", 0.0, False),
1129
+ }
1130
+ fz = MU_LUT_FZ
1131
+ axles: dict[str, Any] = {}
1132
+ runtime: list[dict[str, Any]] = []
1133
+ blockers: list[str] = []
1134
+ used_variables: set[str] = set()
1135
+ for axle in ("front", "rear"):
1136
+ rows = candidates[axle]
1137
+ if len(rows) != 1:
1138
+ reason = f"Expected one selected {axle} tire part; found {len(rows)}."
1139
+ blockers.append(reason)
1140
+ axles[axle] = {"status": "unsupported", "reason": reason,
1141
+ "candidates": [{key: row[key] for key in ("slotType", "partName", "source")}
1142
+ for row in rows]}
1143
+ continue
1144
+ candidate = rows[0]
1145
+ receipts: dict[str, Any] = {}
1146
+ complete = True
1147
+ for field, (unit, minimum, positive) in field_specs.items():
1148
+ receipt, valid = numeric_receipt(candidate["options"].get(field), variables, unit,
1149
+ minimum=minimum, positive=positive)
1150
+ receipts[field] = receipt
1151
+ complete = complete and valid
1152
+ used_variables.update(receipt["variables"])
1153
+ disposition = "source-literal" if receipt["status"] == "literal" else receipt["status"]
1154
+ provenance.append({"target": f"sourceSemantics.wheelsTyres.axles.{axle}.{field}",
1155
+ "source": candidate["source"], "disposition": disposition,
1156
+ "detail": receipt.get("reason") or
1157
+ f"Selected {field} resolved to {receipt.get('value'):g} {unit}."})
1158
+ axle_row = {key: candidate[key] for key in ("slotType", "partName", "source")}
1159
+ axle_row.update(receipts)
1160
+ axle_row["status"] = "functional" if complete else "unsupported"
1161
+ axles[axle] = axle_row
1162
+ if not complete:
1163
+ blockers.append(f"Selected {axle} tire contains unresolved or invalid numeric semantics.")
1164
+ continue
1165
+ values = {field: float(receipt["value"]) for field, receipt in receipts.items()}
1166
+ slide_ratio = values["slidingFrictionCoef"] / values["frictionCoef"]
1167
+ if not 0 < slide_ratio <= 1:
1168
+ receipts["slidingFrictionCoef"]["status"] = "unsupported"
1169
+ receipts["slidingFrictionCoef"]["reason"] = "HELIX muLut.slideRatio must be in (0, 1]."
1170
+ axle_row["status"] = "unsupported"
1171
+ for receipt_provenance in reversed(provenance):
1172
+ if receipt_provenance["target"] == \
1173
+ f"sourceSemantics.wheelsTyres.axles.{axle}.slidingFrictionCoef":
1174
+ receipt_provenance["disposition"] = "unsupported"
1175
+ receipt_provenance["detail"] = receipts["slidingFrictionCoef"]["reason"]
1176
+ break
1177
+ blockers.append(f"Selected {axle} tire sliding/friction ratio is outside (0, 1].")
1178
+ continue
1179
+ mu = [round(values["frictionCoef"] * (values["fullLoadCoef"]
1180
+ + (values["noLoadCoef"] - values["fullLoadCoef"])
1181
+ * math.exp(-values["loadSensitivitySlope"] * load)), 9) for load in fz]
1182
+ runtime.append({
1183
+ **TYRE_SHAPE_PRIOR,
1184
+ "loadSensitivity": {key: values[key] for key in ("frictionCoef", "slidingFrictionCoef",
1185
+ "noLoadCoef", "fullLoadCoef", "loadSensitivitySlope")},
1186
+ "muLut": {"fz": fz, "mu": mu,
1187
+ "slideRatio": round(slide_ratio, 6)},
1188
+ "radius": values["radius"],
1189
+ "calibration": "prior",
1190
+ })
1191
+
1192
+ extraction_complete = not blockers and len(runtime) == 2
1193
+ if extraction_complete:
1194
+ blockers.extend([
1195
+ "Selected pressurePSI is retained but the HELIX tire runtime has no pressure field.",
1196
+ "Selected tireWidth is retained but the HELIX tire runtime has no width field.",
1197
+ ])
1198
+ provenance.append({"target": "simulation.land.tyreByAxle",
1199
+ "source": "selected JBeam pressureWheels", "disposition": "unsupported",
1200
+ "detail": "Radius and load sensitivity use the selected source with the engine-wide exponential 24-knot LUT, but pressure and width have no runtime field; shape coefficients remain explicit priors."})
1201
+ else:
1202
+ provenance.append({"target": "simulation.land.tyreByAxle",
1203
+ "source": "selected JBeam pressureWheels", "disposition": "unsupported",
1204
+ "detail": " ".join(blockers)})
1205
+ fallback = {**TYRE_SHAPE_PRIOR,
1206
+ "loadSensitivity": {"frictionCoef": 1, "slidingFrictionCoef": .85,
1207
+ "noLoadCoef": 1.1, "fullLoadCoef": 1.1, "loadSensitivitySlope": 0},
1208
+ "muLut": {"fz": fz, "mu": [1.1] * MU_LUT_POINTS, "slideRatio": .85},
1209
+ "radius": .3466, "calibration": "prior"}
1210
+ runtime = [fallback, dict(fallback)]
1211
+ functional = False
1212
+ semantics = {"status": "unsupported", "extractionComplete": extraction_complete, "axles": axles,
1213
+ "variables": {name: variable_receipts[name] for name in sorted(used_variables)
1214
+ if name in variable_receipts},
1215
+ "lut": {"fz": fz,
1216
+ "formula": "frictionCoef * (fullLoadCoef + (noLoadCoef - fullLoadCoef) * exp(-loadSensitivitySlope * fz))"},
1217
+ "runtimeBlockers": blockers}
1218
+ return runtime, semantics, functional
1219
+
1220
+
1221
+ def lower_selected_driveline(devices: list[dict[str, Any]],
1222
+ variables: dict[str, Any], variable_receipts: dict[str, dict[str, Any]],
1223
+ provenance: list[dict[str, Any]]) -> tuple[dict[str, dict[str, Any]], dict[str, Any], bool, float]:
1224
+ """Trace selected devices from connected wheels to the gearbox.
1225
+
1226
+ Device names are BeamNG graph identities. Duplicate names, cycles,
1227
+ disconnected halfshafts, unknown differential kinds, and runtime-unsafe
1228
+ parameters keep receipts and suppress every runtime differential. Safe but
1229
+ inexact behavior fields retain an explicitly approximate runtime.
1230
+ """
1231
+ by_name: dict[str, list[dict[str, Any]]] = {}
1232
+ for row in devices:
1233
+ by_name.setdefault(row["name"], []).append(row)
1234
+ blockers = [f"Duplicate powertrain device name {name}."
1235
+ for name, rows in sorted(by_name.items()) if len(rows) != 1]
1236
+ runtime_safe = not blockers
1237
+ unique = {name: rows[0] for name, rows in by_name.items() if len(rows) == 1}
1238
+ transmission_devices = [row for row in devices
1239
+ if str(row.get("type", "")).lower() in TRANSMISSION_DEVICE_KINDS]
1240
+ gearbox_name = transmission_devices[0]["name"] if len(transmission_devices) == 1 else None
1241
+ gearbox_semantics = ({"status": "derived", "deviceName": transmission_devices[0]["name"],
1242
+ "deviceType": transmission_devices[0]["type"],
1243
+ "kind": TRANSMISSION_DEVICE_KINDS[str(transmission_devices[0]["type"]).lower()],
1244
+ "partName": transmission_devices[0]["partName"],
1245
+ "source": transmission_devices[0]["source"]}
1246
+ if len(transmission_devices) == 1 else
1247
+ {"status": "unsupported",
1248
+ "reason": f"Expected one selected recognized transmission device; found {len(transmission_devices)}."})
1249
+ if len(transmission_devices) != 1:
1250
+ blockers.append(f"Expected one selected recognized transmission device; found {len(transmission_devices)}.")
1251
+ runtime_safe = False
1252
+
1253
+ def trace(start: str) -> list[str] | None:
1254
+ path: list[str] = []
1255
+ seen: set[str] = set()
1256
+ current: str | None = start
1257
+ for _ in range(128):
1258
+ if current is None or current in seen:
1259
+ return None
1260
+ seen.add(current)
1261
+ path.append(current)
1262
+ if current == gearbox_name:
1263
+ return path
1264
+ row = unique.get(current)
1265
+ if row is None:
1266
+ return path
1267
+ upstream = row.get("inputName")
1268
+ current = upstream if isinstance(upstream, str) and upstream else None
1269
+ return None
1270
+
1271
+ wheel_starts: dict[str, list[str]] = {}
1272
+ for row in devices:
1273
+ wheel = row.get("connectedWheel")
1274
+ if isinstance(wheel, str) and wheel in ("FL", "FR", "RL", "RR"):
1275
+ wheel_starts.setdefault(wheel, []).append(row["name"])
1276
+ wheel_paths: dict[str, list[str]] = {}
1277
+ for wheel in ("FL", "FR", "RL", "RR"):
1278
+ starts = wheel_starts.get(wheel, [])
1279
+ traced = trace(starts[0]) if len(starts) == 1 else None
1280
+ if traced is not None and gearbox_name is not None and gearbox_name in traced:
1281
+ wheel_paths[wheel] = traced
1282
+ elif len(starts) > 1:
1283
+ blockers.append(f"Ambiguous connected-wheel endpoint {wheel}; found {len(starts)} devices.")
1284
+ runtime_safe = False
1285
+
1286
+ driven_axles: list[str] = []
1287
+ axle_wheels = {"front": ("FL", "FR"), "rear": ("RL", "RR")}
1288
+ for axle, wheels in axle_wheels.items():
1289
+ present = [wheel in wheel_paths for wheel in wheels]
1290
+ if all(present):
1291
+ driven_axles.append(axle)
1292
+ elif any(present):
1293
+ blockers.append(f"Only one selected {axle} wheel has a complete path to gearbox.")
1294
+ runtime_safe = False
1295
+ if not driven_axles:
1296
+ blockers.append("No selected axle has two complete wheel-to-gearbox paths.")
1297
+ runtime_safe = False
1298
+
1299
+ differential_names = {name for name, row in unique.items() if row.get("type") == "differential"}
1300
+ common_all: set[str] = set()
1301
+ if driven_axles:
1302
+ driven_paths = [set(wheel_paths[wheel]) for axle in driven_axles for wheel in axle_wheels[axle]]
1303
+ common_all = set.intersection(*driven_paths) & differential_names
1304
+ axle_diff_names: dict[str, str] = {}
1305
+ for axle in driven_axles:
1306
+ left, right = axle_wheels[axle]
1307
+ candidates = (set(wheel_paths[left]) & set(wheel_paths[right]) & differential_names)
1308
+ if len(driven_axles) == 2:
1309
+ candidates -= common_all
1310
+ if len(candidates) != 1:
1311
+ blockers.append(f"Expected one selected {axle} axle differential; found {len(candidates)}.")
1312
+ runtime_safe = False
1313
+ else:
1314
+ axle_diff_names[axle] = next(iter(candidates))
1315
+ centre_name: str | None = None
1316
+ if len(driven_axles) == 2:
1317
+ candidates = common_all - set(axle_diff_names.values())
1318
+ if len(candidates) != 1:
1319
+ blockers.append(f"Expected one selected AWD centre differential; found {len(candidates)}.")
1320
+ runtime_safe = False
1321
+ else:
1322
+ centre_name = next(iter(candidates))
1323
+
1324
+ selected_diffs = {axle: unique[name] for axle, name in axle_diff_names.items()}
1325
+ if centre_name is not None:
1326
+ selected_diffs["centre"] = unique[centre_name]
1327
+
1328
+ def branch_port(diff_name: str, wheels: tuple[str, ...]) -> int | None:
1329
+ children: set[str] = set()
1330
+ ports: set[int] = set()
1331
+ for wheel in wheels:
1332
+ path = wheel_paths[wheel]
1333
+ index = path.index(diff_name)
1334
+ if index == 0:
1335
+ return None
1336
+ child_name = path[index - 1]
1337
+ child = unique.get(child_name)
1338
+ port = child.get("inputIndex") if child else None
1339
+ if not isinstance(port, int):
1340
+ return None
1341
+ children.add(child_name)
1342
+ ports.add(port)
1343
+ return next(iter(ports)) if len(children) == 1 and len(ports) == 1 else None
1344
+
1345
+ runtime: dict[str, dict[str, Any]] = {}
1346
+ semantics_diffs: dict[str, Any] = {}
1347
+ used_variables: set[str] = set()
1348
+ field_specs = {
1349
+ "lockTorque": "N.m", "activeLockTorque": "N.m",
1350
+ "viscousCoef": "N.m/(rad/s)", "viscousTorque": "N.m", "lsdPreload": "N.m",
1351
+ "lsdLockCoef": "ratio", "lsdRevLockCoef": "ratio", "friction": "N.m",
1352
+ "dynamicFriction": "N.m/(rad/s)", "torqueLossCoef": "ratio",
1353
+ "defaultVirtualInertia": "kg.m^2", "speedLimitCoef": "ratio",
1354
+ }
1355
+ for location, row in selected_diffs.items():
1356
+ dtype = str(row.get("diffType", ""))
1357
+ kind = dtype.lower()
1358
+ if kind not in ("open", "locked", "viscous", "lsd"):
1359
+ blockers.append(f"Selected {location} differential type {dtype or '<missing>'} is unsupported.")
1360
+ runtime_safe = False
1361
+ fields: dict[str, Any] = {}
1362
+ values: dict[str, float] = {}
1363
+ for field, unit in field_specs.items():
1364
+ raw = row.get(field, 0)
1365
+ receipt, valid = numeric_receipt(raw, variables, unit, minimum=0)
1366
+ if field not in row and valid:
1367
+ receipt["status"] = "defaulted"
1368
+ receipt["reason"] = "BeamNG field absent; HELIX neutral value 0."
1369
+ fields[field] = receipt
1370
+ if valid:
1371
+ values[field] = float(receipt["value"])
1372
+ else:
1373
+ blockers.append(f"Selected {location} differential field {field} is unresolved.")
1374
+ runtime_safe = False
1375
+ used_variables.update(receipt["variables"])
1376
+ split_raw = row.get("diffTorqueSplit", .5)
1377
+ split, split_valid = numeric_receipt(split_raw, variables, "ratio", minimum=0)
1378
+ if "diffTorqueSplit" not in row and split_valid:
1379
+ split["status"] = "defaulted"
1380
+ split["reason"] = "BeamNG field absent; equal HELIX split 0.5."
1381
+ if split_valid and float(split["value"]) > 1:
1382
+ split["status"] = "unsupported"
1383
+ split["reason"] = "torque split must be within [0, 1]"
1384
+ split_valid = False
1385
+ fields["diffTorqueSplit"] = split
1386
+ used_variables.update(split["variables"])
1387
+ if not split_valid:
1388
+ blockers.append(f"Selected {location} differential torque split is unresolved or invalid.")
1389
+ runtime_safe = False
1390
+ if kind == "locked" and values.get("lockTorque", 0) <= 0:
1391
+ blockers.append(f"Selected {location} locked differential has no positive lockTorque.")
1392
+ runtime_safe = False
1393
+ active_controllers = [name for name in row.get("controllers", []) if "activeCenterDiff" in name]
1394
+ if values.get("activeLockTorque", 0) > 0 or active_controllers:
1395
+ blockers.append(f"Selected {location} activeLockTorque/controller cannot be represented by the HELIX differential runtime.")
1396
+ for field in ("torqueLossCoef", "defaultVirtualInertia", "speedLimitCoef"):
1397
+ if values.get(field, 0) > 0:
1398
+ blockers.append(
1399
+ f"Selected {location} differential {field} cannot be represented by the HELIX differential runtime."
1400
+ )
1401
+ if kind == "viscous":
1402
+ exponent_raw = row.get("viscousExponent")
1403
+ exponent, exponent_valid = numeric_receipt(exponent_raw, variables, "ratio", positive=True)
1404
+ if exponent_valid and exponent.get("value") != 1:
1405
+ exponent["status"] = "unsupported"
1406
+ exponent["reason"] = "HELIX differential runtime has no viscousExponent field."
1407
+ blockers.append(f"Selected {location} viscousExponent cannot be represented exactly.")
1408
+ elif not exponent_valid:
1409
+ blockers.append(f"Selected {location} viscousExponent is unresolved.")
1410
+ runtime_safe = False
1411
+ fields["viscousExponent"] = exponent
1412
+ used_variables.update(exponent["variables"])
1413
+ if values.get("viscousCoef", 0) <= 0 or values.get("viscousTorque", 0) <= 0:
1414
+ blockers.append(f"Selected {location} viscous differential parameters must be positive.")
1415
+ runtime_safe = False
1416
+ if kind == "lsd" and any(values.get(field, -1) < 0
1417
+ for field in ("lsdPreload", "lsdLockCoef", "lsdRevLockCoef")):
1418
+ blockers.append(f"Selected {location} LSD parameters are invalid.")
1419
+ runtime_safe = False
1420
+ source = row["source"]
1421
+ semantics_diffs[location] = {"deviceName": row["name"], "partName": row["partName"],
1422
+ "source": source, "type": kind, "fields": fields,
1423
+ "controllers": row.get("controllers", []),
1424
+ "overlaySources": row.get("overlaySources", []),
1425
+ "overlayFieldSources": row.get("overlayFieldSources", {}),
1426
+ "overlayConflicts": row.get("overlayConflicts", [])}
1427
+ provenance.append({"target": f"sourceSemantics.driveline.differentials.{location}.type",
1428
+ "source": source,
1429
+ "disposition": "source-literal" if kind in ("open", "locked", "viscous", "lsd") else "unsupported",
1430
+ "detail": f"Selected connected differential declares diffType {dtype or '<missing>'}."})
1431
+
1432
+ ratio_raw = row.get("gearRatio", 1 if location == "centre" else None)
1433
+ ratio_receipt, ratio_valid = numeric_receipt(ratio_raw, variables, "ratio", positive=True)
1434
+ if location == "centre" and "gearRatio" not in row and ratio_valid:
1435
+ ratio_receipt["status"] = "defaulted"
1436
+ ratio_receipt["reason"] = "BeamNG centre gearRatio absent; neutral ratio 1."
1437
+ semantics_diffs[location]["gearRatio"] = ratio_receipt
1438
+ used_variables.update(ratio_receipt["variables"])
1439
+ ratio = float(ratio_receipt.get("value", 1))
1440
+ if not ratio_valid:
1441
+ blockers.append(f"Selected {location} differential gearRatio is absent or invalid.")
1442
+ runtime_safe = False
1443
+
1444
+ if location == "centre":
1445
+ a_port = branch_port(row["name"], ("FL", "FR"))
1446
+ b_port = branch_port(row["name"], ("RL", "RR"))
1447
+ elif location == "front":
1448
+ a_port = branch_port(row["name"], ("FL",))
1449
+ b_port = branch_port(row["name"], ("FR",))
1450
+ else:
1451
+ a_port = branch_port(row["name"], ("RL",))
1452
+ b_port = branch_port(row["name"], ("RR",))
1453
+ semantics_diffs[location]["outputPorts"] = {"a": a_port, "b": b_port}
1454
+ if {a_port, b_port} != {1, 2}:
1455
+ blockers.append(f"Selected {location} differential output ports are missing, duplicate, or ambiguous.")
1456
+ runtime_safe = False
1457
+ if kind in ("open", "locked", "viscous", "lsd"):
1458
+ source_split = float(split.get("value", .5))
1459
+ runtime_split = source_split if a_port == 1 else round(1 - source_split, 8)
1460
+ runtime[location] = diff(kind, ratio, preload=values.get("lsdPreload", 0),
1461
+ lock=values.get("lsdLockCoef", 0),
1462
+ reverse=values.get("lsdRevLockCoef", 0), torque_split=runtime_split,
1463
+ friction=values.get("friction", 0),
1464
+ dynamic=values.get("dynamicFriction", 0),
1465
+ lock_torque=values.get("lockTorque", 0),
1466
+ viscous_coef=values.get("viscousCoef", 0),
1467
+ viscous_torque=values.get("viscousTorque", 0))
1468
+
1469
+ functional = not blockers and len(selected_diffs) == len(driven_axles) + (1 if len(driven_axles) == 2 else 0)
1470
+ if len(selected_diffs) != len(driven_axles) + (1 if len(driven_axles) == 2 else 0):
1471
+ runtime_safe = False
1472
+ if not runtime_safe:
1473
+ runtime = {}
1474
+ if driven_axles == ["front"]:
1475
+ bias_front = 1.0
1476
+ elif driven_axles == ["rear"]:
1477
+ bias_front = 0.0
1478
+ elif centre_name and isinstance(selected_diffs.get("centre"), dict):
1479
+ centre = semantics_diffs["centre"]
1480
+ centre_split = centre["fields"]["diffTorqueSplit"]
1481
+ if centre_split["status"] != "unsupported":
1482
+ source_split = float(centre_split["value"])
1483
+ bias_front = source_split if centre["outputPorts"]["a"] == 1 else round(1 - source_split, 8)
1484
+ else:
1485
+ bias_front = .5
1486
+ else:
1487
+ bias_front = .5
1488
+ if not runtime:
1489
+ bias_front = .5
1490
+ axle_ratios = [row["gearRatio"] for location, row in semantics_diffs.items()
1491
+ if location in ("front", "rear") and row["gearRatio"]["status"] != "unsupported"]
1492
+ unique_axle_ratios = {float(row["value"]) for row in axle_ratios}
1493
+ if len(axle_ratios) == len(driven_axles) and len(unique_axle_ratios) == 1:
1494
+ legacy_final_drive = {"raw": [row["raw"] for row in axle_ratios], "status": "derived",
1495
+ "unit": "ratio", "variables": sorted({variable for row in axle_ratios
1496
+ for variable in row["variables"]}),
1497
+ "value": next(iter(unique_axle_ratios))}
1498
+ else:
1499
+ legacy_final_drive = {"raw": [row.get("raw") for row in axle_ratios], "status": "unsupported",
1500
+ "unit": "ratio", "variables": sorted({variable for row in axle_ratios
1501
+ for variable in row["variables"]}),
1502
+ "reason": "Driven axle ratios are missing or unequal; one legacy finalDrive cannot represent them."}
1503
+ provenance.append({"target": "simulation.land.driveline.differentials",
1504
+ "source": "selected JBeam powertrain graph",
1505
+ "disposition": "derived" if functional else "unsupported",
1506
+ "detail": "Wheel endpoints traced to gearbox without synthesized axles or centre differential."
1507
+ if functional else " ".join(blockers)})
1508
+ semantics = {"status": "functional" if functional else "unsupported",
1509
+ "runtimeStatus": "functional" if functional else "approximate" if runtime else "unsupported",
1510
+ "gearbox": gearbox_semantics, "transmissionDevice": gearbox_name,
1511
+ "legacyFinalDrive": legacy_final_drive,
1512
+ "drivenAxles": driven_axles, "wheelPaths": wheel_paths,
1513
+ "differentials": semantics_diffs,
1514
+ "variables": {name: variable_receipts[name] for name in sorted(used_variables)
1515
+ if name in variable_receipts},
1516
+ "runtimeBlockers": blockers}
1517
+ return runtime, semantics, functional, bias_front
1518
+
1519
+
1520
+ def main() -> None:
1521
+ parser = argparse.ArgumentParser()
1522
+ parser.add_argument("--source-root", required=True)
1523
+ parser.add_argument("--vehicle-root", required=True)
1524
+ parser.add_argument("--configuration", required=True)
1525
+ parser.add_argument("--vehicle-source", required=True)
1526
+ parser.add_argument("--output", required=True)
1527
+ parser.add_argument("--geometry")
1528
+ parser.add_argument("--part-index")
1529
+ parser.add_argument("--abs-controller-receipt")
1530
+ parser.add_argument("--common-root", action="append", default=[])
1531
+ parser.add_argument("--engine-override", help="JSON file {\"cylinders\": N, \"reason\": ...}: operator-declared engine facts "
1532
+ "(the beamngEngine Bridge input)")
1533
+ parser.add_argument("--audio-dir", help="where the mod's own engine recordings are written as PCM WAV "
1534
+ "(default: <output dir>/audio)")
1535
+ args = parser.parse_args()
1536
+
1537
+ source_root = Path(args.source_root).resolve()
1538
+ vehicle_root = Path(args.vehicle_root).resolve()
1539
+ config_path = Path(args.configuration).resolve()
1540
+ manifest_path = Path(args.vehicle_source).resolve()
1541
+ if not vehicle_root.is_dir() or not config_path.is_file() or not manifest_path.is_file():
1542
+ fail("source, vehicle, configuration, and vehicle-source inputs must exist")
1543
+ try:
1544
+ vehicle_root.relative_to(source_root)
1545
+ config_path.relative_to(vehicle_root)
1546
+ except ValueError:
1547
+ fail("vehicle root and configuration must remain inside source root")
1548
+
1549
+ source_manifest = json.loads(manifest_path.read_text())
1550
+ if source_manifest.get("schemaVersion") != SOURCE_SCHEMA:
1551
+ fail(f"vehicle source must use {SOURCE_SCHEMA}")
1552
+ candidate = vehicle_root.name
1553
+ if source_manifest.get("vehicle", {}).get("candidateId") != candidate:
1554
+ fail("vehicle-source candidate identity does not match vehicle root")
1555
+ identity = source_manifest.get("identity", {})
1556
+ plan_key, source_sha = identity.get("bridgePlanKey"), identity.get("sourceSha256")
1557
+ if not re.fullmatch(r"[0-9a-f]{64}", str(plan_key)) or not re.fullmatch(r"[0-9a-f]{64}", str(source_sha)):
1558
+ fail("vehicle-source identity requires lowercase sha256 bridgePlanKey and sourceSha256")
1559
+
1560
+ config = json.loads(config_path.read_text())
1561
+ if config.get("format") != 2 or not isinstance(config.get("parts"), dict):
1562
+ fail("configuration must be BeamNG .pc format 2 with a parts object")
1563
+ if "vars" in config and not isinstance(config["vars"], dict):
1564
+ fail("configuration vars must be an object when present")
1565
+ config_model = config.get("model") or config.get("mainPartName") or candidate
1566
+ if config_model != candidate:
1567
+ fail("configuration model/mainPartName does not match vehicle-source candidate")
1568
+ source_configs = source_manifest.get("sourceFiles", {}).get("configuration")
1569
+ expected_config = source_manifest.get("configuration", {}).get("file")
1570
+ if not isinstance(source_configs, list) or len(source_configs) != 1 or expected_config != config_path.name:
1571
+ fail("vehicle-source must fingerprint this exact selected configuration")
1572
+ config_receipt = source_configs[0]
1573
+ if config_receipt.get("root", "source") != "source" or config_receipt.get("path") != rel(config_path, source_root):
1574
+ fail("vehicle-source configuration path does not match selected .pc")
1575
+ config_sha, _config_size = sha256_file(config_path)
1576
+ if config_receipt.get("sha256") != config_sha or config_receipt.get("sizeBytes") != _config_size:
1577
+ fail("vehicle-source configuration sha256 does not match selected .pc")
1578
+ config_info: dict[str, Any] = {}
1579
+ config_info_path = vehicle_root / f"info_{config_path.stem}.json"
1580
+ if config_info_path.is_file():
1581
+ receipts = source_manifest.get("sourceFiles", {}).get("configurationInfo")
1582
+ if not isinstance(receipts, list) or len(receipts) != 1:
1583
+ fail("vehicle-source must fingerprint the consumed configuration info")
1584
+ info_sha, info_size = sha256_file(config_info_path)
1585
+ if receipts[0].get("root", "source") != "source" \
1586
+ or receipts[0].get("path") != rel(config_info_path, source_root) or receipts[0].get("sha256") != info_sha \
1587
+ or receipts[0].get("sizeBytes") != info_size:
1588
+ fail("configuration info receipt does not match selected source bytes")
1589
+ config_info = json.loads(config_info_path.read_text())
1590
+ explicit = [(str(k), str(v)) for k, v in config["parts"].items() if isinstance(v, str) and v]
1591
+ explicit.sort()
1592
+ selected_names = {v for _, v in explicit}
1593
+
1594
+ dependency_roots = [Path(value).resolve() for value in args.common_root]
1595
+ root_map = {"source": source_root, **{f"dependency:{index}": root
1596
+ for index, root in enumerate(dependency_roots)}}
1597
+ common_closure = identity.get("commonDependency", {}).get("closureSha256") \
1598
+ if isinstance(identity.get("commonDependency"), dict) else None
1599
+
1600
+ def source_label(path: Path) -> str:
1601
+ resolved = path.resolve()
1602
+ for origin, root in root_map.items():
1603
+ try:
1604
+ portable = resolved.relative_to(root).as_posix()
1605
+ return portable if origin == "source" else f"{origin}:{portable}@sha256:{common_closure}"
1606
+ except ValueError:
1607
+ continue
1608
+ fail(f"selected source escapes admitted roots: {path}")
1609
+ return "unreachable"
1610
+ ranked_roots = [(vehicle_root, 0)]
1611
+ local_common = source_root / "vehicles" / "common"
1612
+ if local_common.is_dir():
1613
+ ranked_roots.append((local_common, 1))
1614
+ ranked_roots.extend((root / "vehicles" / "common", 2) for root in dependency_roots
1615
+ if (root / "vehicles" / "common").is_dir())
1616
+ roots = [root for root, _ in ranked_roots]
1617
+ definitions: dict[str, tuple[dict[str, Any], Path]] = {}
1618
+ parse_failures: list[str] = []
1619
+ lua_files: list[str] = []
1620
+ parsed_files = 0
1621
+ indexed_files = 0
1622
+ producer_parsed_files = 0
1623
+ maximum_parses_per_file = 0
1624
+ selected_slot_rows: list[dict[str, Any]] = []
1625
+ if args.part_index:
1626
+ index_path = Path(args.part_index).resolve()
1627
+ if (not index_path.is_file() or index_path.is_symlink()
1628
+ or index_path.stat().st_size > MAX_SELECTED_PART_INDEX_BYTES):
1629
+ fail("selected-part index must be a bounded regular non-symlink file")
1630
+ try:
1631
+ with index_path.open("r", encoding="utf-8") as handle:
1632
+ index = json.load(handle)
1633
+ except (UnicodeDecodeError, json.JSONDecodeError):
1634
+ fail("selected-part index is not canonical UTF-8 JSON")
1635
+ actual_digest, actual_size = sha256_file(index_path)
1636
+ canonical_digest, canonical_size = canonical_json_receipt(index)
1637
+ if (actual_digest, actual_size) != (canonical_digest, canonical_size):
1638
+ fail("selected-part index bytes are not canonical deterministic JSON")
1639
+ index_receipt = source_manifest.get("configuration", {}).get("selectedPartIndex")
1640
+ if index.get("schemaVersion") != SELECTED_PART_INDEX_SCHEMA or not isinstance(index_receipt, dict) \
1641
+ or index_receipt.get("schemaVersion") != SELECTED_PART_INDEX_SCHEMA \
1642
+ or index_receipt.get("sha256") != actual_digest or index_receipt.get("sizeBytes") != actual_size:
1643
+ fail("selected-part index schema or digest does not match vehicle-source receipt")
1644
+ if index.get("candidateId") != candidate or index.get("configurationSha256") != config_sha:
1645
+ fail("selected-part index identity does not match selected vehicle/configuration")
1646
+ if index.get("configurationParts") != config.get("parts") \
1647
+ or index.get("configurationMainPartName") != config.get("mainPartName") \
1648
+ or index.get("configurationModel") != config.get("model"):
1649
+ fail("selected-part index configuration body does not match selected .pc")
1650
+ index_identity = index.get("identity")
1651
+ if not isinstance(index_identity, dict) or index_identity.get("bridgePlanKey") != plan_key \
1652
+ or index_identity.get("sourceSha256") != source_sha \
1653
+ or index_identity.get("commonDependencyClosureSha256") != common_closure:
1654
+ fail("selected-part index is not bound to this Bridge plan/source/common closure")
1655
+ indexed_config = index.get("configuration")
1656
+ if not isinstance(indexed_config, dict) or indexed_config.get("root") != "source" \
1657
+ or indexed_config.get("path") != rel(config_path, source_root) \
1658
+ or indexed_config.get("sha256") != config_sha or indexed_config.get("sizeBytes") != _config_size:
1659
+ fail("selected-part index configuration receipt does not match selected .pc bytes")
1660
+ sources = index.get("sources")
1661
+ parts = index.get("parts")
1662
+ if not isinstance(sources, list) or not isinstance(parts, list) \
1663
+ or len(sources) > MAX_INDEX_SOURCES or len(parts) > MAX_INDEX_PARTS:
1664
+ fail("selected-part index requires sources and parts arrays")
1665
+ if index_receipt.get("sourceCount") != len(sources) or index_receipt.get("partCount") != len(parts):
1666
+ fail("selected-part index receipt counts do not match its closure")
1667
+ manifest_jbeams = source_manifest.get("sourceFiles", {}).get("jbeam")
1668
+ if not isinstance(manifest_jbeams, list):
1669
+ fail("vehicle-source must fingerprint the selected source JBeam closure")
1670
+ manifest_source_receipts = {(row.get("root", "source"), row.get("path")): row
1671
+ for row in manifest_jbeams if isinstance(row, dict)}
1672
+ if len(manifest_source_receipts) != len(manifest_jbeams):
1673
+ fail("vehicle-source contains duplicate or invalid JBeam receipts")
1674
+ verified_sources: dict[tuple[str, str], Path] = {}
1675
+ for row in sources:
1676
+ if not isinstance(row, dict) or row.get("root") not in root_map:
1677
+ fail("selected-part index source uses an unadmitted root")
1678
+ origin, portable = row["root"], portable_index_path(row.get("path"))
1679
+ key = (origin, portable)
1680
+ if key in verified_sources:
1681
+ fail("selected-part index repeats a source receipt")
1682
+ path = confined_file(root_map[origin], portable)
1683
+ digest, size = sha256_file(path)
1684
+ if digest != row.get("sha256") or size != row.get("sizeBytes"):
1685
+ fail("selected-part index source fingerprint changed")
1686
+ if origin == "source":
1687
+ manifest_row = manifest_source_receipts.get(key)
1688
+ if manifest_row is None or manifest_row.get("sha256") != digest or manifest_row.get("sizeBytes") != size:
1689
+ fail("selected-part index source is not bound by the vehicle-source JBeam receipts")
1690
+ verified_sources[key] = path
1691
+ selected_source_keys = {key for key in verified_sources if key[0] == "source"}
1692
+ if set(manifest_source_receipts) != selected_source_keys:
1693
+ fail("vehicle-source JBeam receipts contain files outside the selected closure")
1694
+ selected_bodies = index.get("selectedBodies")
1695
+ if not isinstance(selected_bodies, list) or len(selected_bodies) != len(set(selected_bodies)):
1696
+ fail("selected-part index selectedBodies must be a unique ordered list")
1697
+ used_sources: set[tuple[str, str]] = set()
1698
+ for row in parts:
1699
+ if not isinstance(row, dict) or not isinstance(row.get("name"), str) or not isinstance(row.get("body"), dict):
1700
+ fail("selected-part index contains an invalid part row")
1701
+ source_key = (row.get("sourceRoot"), row.get("source"))
1702
+ source_path = verified_sources.get(source_key)
1703
+ if source_path is None or row["name"] in definitions:
1704
+ fail("selected-part index contains an unknown source or duplicate part")
1705
+ definitions[row["name"]] = (row["body"], source_path)
1706
+ used_sources.add(source_key)
1707
+ if selected_bodies != [row.get("name") for row in parts] or used_sources != set(verified_sources):
1708
+ fail("selected-part index parts, selected bodies, and source receipts do not form one exact closure")
1709
+ parse_counts = index.get("parseCounts", {})
1710
+ if parse_counts and (not isinstance(parse_counts, dict) or any(value != 1 for value in parse_counts.values())):
1711
+ fail("selected-part index does not prove one parse per consumed document")
1712
+ indexed_files = len(verified_sources)
1713
+ producer_parsed_files = len(parse_counts)
1714
+ maximum_parses_per_file = max(parse_counts.values(), default=0)
1715
+ selected_slot_rows = [row for row in index.get("slotClosure", []) if isinstance(row, dict)]
1716
+ selected = [(str(row.get("slotType") or "$selected"), row["name"], row["body"],
1717
+ verified_sources[(row["sourceRoot"], row["source"])]) for row in parts]
1718
+ closure_rows = [{"slotType": "$main", "partName": str(index.get("mainPartName")),
1719
+ "selection": "root", "resolution": "resolved"}]
1720
+ closure_rows.extend({"slotType": str(row.get("slotType")), "partName": str(row.get("selectedPart")),
1721
+ "selection": str(row.get("selection")), "resolution": str(row.get("resolution"))}
1722
+ for row in index.get("slotClosure", []) if isinstance(row, dict)
1723
+ and isinstance(row.get("slotType"), str) and isinstance(row.get("selectedPart"), str)
1724
+ and row.get("selectedPart") not in ("", "none"))
1725
+ missing = [(str(row.get("slotType")), str(row.get("partName")))
1726
+ for row in index.get("missing", []) if isinstance(row, dict)]
1727
+ missing.extend((row["slotType"], row["partName"]) for row in closure_rows
1728
+ if row["resolution"] == "missing-source")
1729
+ else:
1730
+ cache = JBeamDocumentCache(source_root, dependency_roots)
1731
+ catalog, parse_failures = build_part_catalog(
1732
+ roots, cache, [rank for _, rank in ranked_roots])
1733
+ graph = resolve_part_closure(catalog, dict(explicit), str(config.get("mainPartName") or candidate))
1734
+ selected_slot_rows = graph["slotClosure"]
1735
+ for row in graph["parts"]:
1736
+ definitions[row["name"]] = (catalog.body(row["name"]), row["_sourcePath"])
1737
+ selected = [(str(row.get("slotType") or "$selected"), row["name"],
1738
+ definitions[row["name"]][0], row["_sourcePath"])
1739
+ for row in graph["parts"]]
1740
+ closure_rows = [{"slotType": "$main", "partName": str(graph.get("mainPartName")),
1741
+ "selection": "root", "resolution": "resolved"}]
1742
+ closure_rows.extend({"slotType": str(row.get("slotType")), "partName": str(row.get("selectedPart")),
1743
+ "selection": str(row.get("selection")), "resolution": str(row.get("resolution"))}
1744
+ for row in graph["slotClosure"] if row.get("selectedPart") not in (None, "", "none"))
1745
+ missing = [(str(row.get("slotType")), str(row.get("partName"))) for row in graph["missing"]]
1746
+ parsed_files = sum(cache.parse_counts.values())
1747
+ producer_parsed_files = len(cache.parse_counts)
1748
+ maximum_parses_per_file = max(cache.parse_counts.values(), default=0)
1749
+ for root in roots:
1750
+ lua_files.extend(rel(path, source_root) for path in bounded_asset_paths(
1751
+ [root], {".lua": LUA_SCAN_LIMIT}))
1752
+
1753
+ closure_rows.sort(key=lambda row: (row["slotType"], row["partName"]))
1754
+ selected_names = {part_name for _, part_name, _, _ in selected}
1755
+ selected_bodies = [body for _, _, body, _ in selected]
1756
+ if not args.part_index:
1757
+ receipts = source_manifest.get("sourceFiles", {}).get("jbeam")
1758
+ if not isinstance(receipts, list):
1759
+ fail("vehicle-source must fingerprint every consumed JBeam file")
1760
+ receipt_by_path: dict[str, dict[str, Any]] = {}
1761
+ for row in receipts:
1762
+ portable = row.get("path") if isinstance(row, dict) else None
1763
+ if not isinstance(portable, str) or portable in receipt_by_path:
1764
+ fail("vehicle-source has an invalid or duplicate JBeam receipt")
1765
+ receipt_by_path[portable] = row
1766
+ for path in sorted({path.resolve() for _, _, _, path in selected}):
1767
+ portable = rel(path, source_root)
1768
+ row = receipt_by_path.get(portable)
1769
+ digest, size = sha256_file(path)
1770
+ if row is None or row.get("sha256") != digest or row.get("sizeBytes") != size:
1771
+ fail(f"consumed JBeam receipt does not match source bytes: {portable}")
1772
+ value_index = key_multimap((part_name, body) for _, part_name, body, _ in selected)
1773
+ node_masses: dict[str, float] = {}
1774
+ for body in selected_bodies:
1775
+ for row in decode_jbeam_table(body.get("nodes")):
1776
+ node_id, node_weight = row.get("id"), row.get("nodeWeight")
1777
+ if isinstance(node_id, str) and isinstance(node_weight, (int, float)) and node_weight > 0:
1778
+ node_masses[node_id] = float(node_weight)
1779
+ source_mass = round(sum(node_masses.values()), 4)
1780
+
1781
+ citation_id = "beamng-jbeam-source"
1782
+ provenance: list[dict[str, Any]] = []
1783
+ def prov(target: str, source: str, disposition: str, detail: str) -> None:
1784
+ provenance.append({"target": target, "source": source, "disposition": disposition, "detail": detail})
1785
+ for slot_name, part_name, _, path in selected:
1786
+ prov(f"build.selections.{slot_name}", source_label(path), "source-literal", part_name)
1787
+ for slot_name, part_name in missing:
1788
+ prov(f"build.selections.{slot_name}", f"{config_path.name}#parts.{slot_name}", "missing-source",
1789
+ f"Selected part {part_name} is not present in the admitted JBeam closure.")
1790
+
1791
+ # Engine values come from the chosen engine plus selected ECU/internals overlays.
1792
+ engine_parts = [(n, b, p) for _, n, b, p in selected if "engine" in n.lower()]
1793
+ engine_node = [b for _, b, _ in engine_parts]
1794
+ engine_configs = [body.get("mainEngine", {}) for body in engine_node if isinstance(body.get("mainEngine"), dict)]
1795
+ torque: list[list[float]] = []
1796
+ for body in engine_configs:
1797
+ values = body.get("torque")
1798
+ if isinstance(values, list):
1799
+ for row in values[1:] if values and isinstance(values[0], list) and values[0][:2] == ["rpm", "torque"] else values:
1800
+ if isinstance(row, list) and len(row) >= 2 and all(isinstance(x, (int, float)) for x in row[:2]):
1801
+ torque.append([float(row[0]), float(row[1])])
1802
+ torque = sorted({(x[0], x[1]) for x in torque})
1803
+ torque_curve = [[int(r) if r.is_integer() else r, int(t) if t.is_integer() else t] for r, t in torque]
1804
+ if len(torque_curve) < 2:
1805
+ torque_curve = [[800, 100], [7000, 80]]
1806
+ prov("physics.engine.torqueCurve", "HELIX functional fallback", "defaulted", "No literal torque table in selected closure.")
1807
+ else:
1808
+ prov("physics.engine.torqueCurve", source_label(engine_parts[0][2]), "source-literal", "BeamNG torque table.")
1809
+ engine_filter = lambda part: "engine" in part.lower()
1810
+ idle, idle_key = number(value_index, ["idleRPM", "idleRpm"], 800, engine_filter)
1811
+ limiter, limiter_key = number(value_index, ["revLimiterRPM", "maxRPM", "limiterRpm"], 7000, engine_filter)
1812
+ inertia, _ = number(value_index, ["inertia"], .1, engine_filter)
1813
+ friction, _ = number(value_index, ["friction"], 11.5, engine_filter)
1814
+ dyn_friction, _ = number(value_index, ["dynamicFriction"], .024, engine_filter)
1815
+ coast, _ = number(value_index, ["engineBrakeTorque"], 38, engine_filter)
1816
+ for target, key in [("idleRpm", idle_key), ("limiterRpm", limiter_key)]:
1817
+ prov(f"physics.engine.{target}", f"selected JBeam#{key}", "source-literal" if key != "HELIX-default" else "defaulted", str(key))
1818
+ peak_rpm, peak_torque = max(torque_curve, key=lambda row: row[1])
1819
+ power_points = [(r * t / 9549.296596, r) for r, t in torque_curve]
1820
+ peak_power, peak_power_rpm = max(power_points)
1821
+ published_power = float(config_info["Power"]) if isinstance(config_info.get("Power"), (int, float)) else None
1822
+ published_torque = float(config_info["Torque"]) if isinstance(config_info.get("Torque"), (int, float)) else None
1823
+ if published_power is not None:
1824
+ prov("physics.engine.peakPowerKw", f"info_{config_path.stem}.json#Power", "source-literal",
1825
+ f"Selected configuration publishes {published_power:g} kW.")
1826
+ if published_torque is not None:
1827
+ prov("physics.engine.peakTorqueNm", f"info_{config_path.stem}.json#Torque", "source-literal",
1828
+ f"Selected configuration publishes {published_torque:g} N.m.")
1829
+
1830
+ def range_midpoint(value: Any, fallback: float) -> float:
1831
+ values = [float(item) for item in re.findall(r"\d+(?:\.\d+)?", str(value))]
1832
+ return sum(values) / len(values) if values else fallback
1833
+
1834
+ published_power_rpm = range_midpoint(config_info.get("PowerPeakRPM"), peak_power_rpm)
1835
+ published_torque_rpm = range_midpoint(config_info.get("TorquePeakRPM"), peak_rpm)
1836
+
1837
+ power_rows = selected_powertrain_devices(selected, source_label)
1838
+ engine_device = next((r for r in power_rows if r.get("type") == "combustionEngine"), {})
1839
+ transmission_kind, transmission_source, transmission_detail = selected_transmission_kind(
1840
+ power_rows, config_info)
1841
+ transmission_kind_sourced = transmission_kind is not None
1842
+ if transmission_kind_sourced:
1843
+ prov("simulation.land.driveline.gearbox.kind", transmission_source, "source-literal", transmission_detail)
1844
+ else:
1845
+ transmission_kind = "manual"
1846
+ prov("simulation.land.driveline.gearbox.kind", transmission_source, "discovered-not-lowered",
1847
+ f"{transmission_detail} HELIX emits a manual fallback without claiming gearbox coverage.")
1848
+ ratios: list[float] = []
1849
+ reverse = -3.545
1850
+ component_ratios: dict[str, float] = {}
1851
+ for value in indexed_values(value_index, "gearRatios"):
1852
+ if isinstance(value, dict):
1853
+ component_ratios.update({str(key): float(ratio) for key, ratio in value.items()
1854
+ if isinstance(ratio, (int, float)) and re.fullmatch(r"gearRatio_(?:R|\d+)", str(key))})
1855
+ elif isinstance(value, list):
1856
+ nums = [float(x) for x in value if isinstance(x, (int, float))]
1857
+ if len(nums) >= 2:
1858
+ ratios = [x for x in nums if x > 0]
1859
+ reverse_candidates = [x for x in nums if x < 0]
1860
+ if reverse_candidates: reverse = reverse_candidates[0]
1861
+ else:
1862
+ continue
1863
+ if component_ratios:
1864
+ ratios = [component_ratios[key] for key in sorted(component_ratios,
1865
+ key=lambda key: int(key.removeprefix("gearRatio_")) if key.removeprefix("gearRatio_").isdigit() else 10_000)
1866
+ if key != "gearRatio_R" and component_ratios[key] > 0]
1867
+ if "gearRatio_R" in component_ratios:
1868
+ reverse = -abs(component_ratios["gearRatio_R"])
1869
+ ratios_sourced = bool(ratios)
1870
+ ratios_derived = False
1871
+ if transmission_kind == "cvt" and not ratios:
1872
+ cvt_bodies = [body for _, _, body, _ in selected if isinstance(body.get("gearbox"), dict)]
1873
+ cvt_min = next((float(body["gearbox"]["minGearRatio"]) for body in cvt_bodies
1874
+ if isinstance(body["gearbox"].get("minGearRatio"), (int, float))), None)
1875
+ cvt_max = next((float(body["gearbox"]["maxGearRatio"]) for body in cvt_bodies
1876
+ if isinstance(body["gearbox"].get("maxGearRatio"), (int, float))), None)
1877
+ if cvt_min is not None and cvt_max is not None and 0 < cvt_min <= cvt_max:
1878
+ ratios = [cvt_max, cvt_min]
1879
+ ratios_derived = True
1880
+ if not ratios:
1881
+ ratios = [3.636, 2.111, 1.488, 1.088, .825]
1882
+ prov("physics.gearbox.ratios", "HELIX known-safe manual prior", "defaulted", "No literal gearRatios table found.")
1883
+ elif ratios_derived:
1884
+ prov("physics.gearbox.ratios", transmission_source, "derived",
1885
+ "CVT maxGearRatio/minGearRatio retained as runtime range endpoints; continuous-ratio behavior is not yet lowered.")
1886
+ else:
1887
+ prov("physics.gearbox.ratios", "selected JBeam#components.gearRatios", "source-literal",
1888
+ "Selected ratio component dictionaries merged by numbered gear key.")
1889
+ reverse_value, reverse_key = ((reverse, "components.gearRatios") if "gearRatio_R" in component_ratios
1890
+ else number(value_index, ["reverseGearRatio", "reverseRatio"], reverse))
1891
+ reverse = -abs(reverse_value)
1892
+ final_drive, fd_key = number(value_index, ["gearRatio"], 4.11, lambda part: "finaldrive" in part.lower())
1893
+ shift_delay, shift_key = number(value_index, ["transmissionShiftDelay", "shiftDelay"], .25)
1894
+
1895
+ source_ref = citation_id
1896
+ selected_variables, selected_variable_receipts = selected_variable_environment(
1897
+ selected, config, config_path, source_label)
1898
+ brakes, brake_semantics, brakes_functional = lower_selected_brakes(
1899
+ selected, selected_variables, selected_variable_receipts,
1900
+ source_label, citation_id, provenance)
1901
+ abs_runtime, abs_semantics, abs_adapted = lower_selected_abs(
1902
+ selected, config_path.name, config_sha,
1903
+ Path(args.abs_controller_receipt).resolve() if args.abs_controller_receipt else None,
1904
+ source_label, provenance)
1905
+ chassis_mass = (float(config_info["Weight"])
1906
+ if isinstance(config_info.get("Weight"), (int, float)) else
1907
+ source_mass if source_mass > 100 else 1365.0)
1908
+ weight_distribution_front = .58
1909
+ suspension, suspension_semantics = lower_selected_suspension(
1910
+ selected, selected_variables, selected_variable_receipts, source_label,
1911
+ chassis_mass, weight_distribution_front, provenance)
1912
+ differential_runtime, driveline_semantics, differentials_functional, drive_bias_front = lower_selected_driveline(
1913
+ power_rows, selected_variables, selected_variable_receipts, provenance)
1914
+ legacy_final_drive = driveline_semantics["legacyFinalDrive"]
1915
+ if legacy_final_drive["status"] != "unsupported":
1916
+ final_drive = float(legacy_final_drive["value"])
1917
+ fd_key = "selected-driveline-final-drive"
1918
+ tyre_by_axle, tyre_semantics, tyres_functional = lower_selected_tyres(
1919
+ selected, selected_variables, selected_variable_receipts, source_label, provenance)
1920
+ turbo_parts = [(name, body, path) for _, name, body, path in selected if isinstance(body.get("turbocharger"), dict)]
1921
+ wastegate_psi = None
1922
+ for _, name, body, _ in selected:
1923
+ if "ecu" not in name.lower():
1924
+ continue
1925
+ for row in decode_jbeam_table(body.get("variables")):
1926
+ if row.get("name") == "$wastegateTarget" and isinstance(row.get("default"), (int, float)):
1927
+ wastegate_psi = float(row["default"])
1928
+ induction = None
1929
+ if str(config_info.get("Induction Type", "")).lower() == "turbo" and turbo_parts and wastegate_psi:
1930
+ turbocharger = turbo_parts[0][1]["turbocharger"]
1931
+ pressure_rows = [row for row in turbocharger.get("pressurePSI", [])
1932
+ if isinstance(row, list) and len(row) >= 2 and isinstance(row[1], (int, float))]
1933
+ max_pressure_psi = max((float(row[1]) for row in pressure_rows), default=wastegate_psi)
1934
+ engine_def = [row for row in turbocharger.get("engineDef", [])
1935
+ if isinstance(row, list) and len(row) >= 2 and all(isinstance(value, (int, float)) for value in row[:2])]
1936
+ reference_rpm = float(max(engine_def, key=lambda row: row[1])[0]) if engine_def else 4000
1937
+ pressure_rate = float(turbocharger.get("pressureRatePSI", 50) or 50)
1938
+ wastegate_bar = round(wastegate_psi * 0.0689475729, 6)
1939
+ max_boost_bar = round(max(max_pressure_psi, wastegate_psi) * 0.0689475729, 6)
1940
+ induction = {"kind": "turbo", "maxBoostBar": sourced(max_boost_bar, "bar", source_ref),
1941
+ "wastegateBar": sourced(wastegate_bar, "bar", source_ref),
1942
+ "referenceRpm": sourced(reference_rpm, "rpm", source_ref),
1943
+ "gamma": sourced(1.8, "ratio", note="HELIX turbo response fit for the selected BeamNG pressure/engine efficiency tables."),
1944
+ "spoolUpTime": sourced(round(wastegate_psi / max(pressure_rate, 0.001), 4), "s", source_ref),
1945
+ "spoolDownTime": sourced(round(wastegate_psi / max(pressure_rate, 0.001) * 1.4, 4), "s", note="Selected pressure rise time multiplied by HELIX decay prior 1.4."),
1946
+ "blowOffThresholdBar": sourced(round(wastegate_bar * .35, 6), "bar", note="35% of selected wastegate pressure."),
1947
+ "torqueCurveIncludesBoost": False}
1948
+ prov("physics.induction", source_label(turbo_parts[0][2]),
1949
+ "source-literal", f"Turbo selected with {wastegate_psi:g} psi wastegate target.")
1950
+ elif "turbo" in str(config_info.get("Induction Type", "")).lower():
1951
+ prov("physics.induction", f"info_{config_path.stem}.json#Induction Type", "discovered-not-lowered",
1952
+ "Selected configuration declares forced induction but exact turbo and ECU literals were incomplete.")
1953
+
1954
+ # Geometry can be supplied from Blender's authoritative receipt.
1955
+ geometry: dict[str, Any] = {}
1956
+ if args.geometry:
1957
+ document = json.loads(Path(args.geometry).read_text())
1958
+ geometry = document.get("HELIX_vehicle", document)
1959
+ sockets = geometry.get("sockets") if isinstance(geometry.get("sockets"), dict) else {}
1960
+ selected_tyre_radius = sum(float(row["radius"]) for row in tyre_by_axle) / len(tyre_by_axle)
1961
+ wheel_radius = float(geometry.get("wheelRadius", selected_tyre_radius))
1962
+ bounds = geometry.get("bounds", {})
1963
+ extents = bounds.get("size") if isinstance(bounds, dict) else None
1964
+ if (not isinstance(extents, list) or len(extents) != 3) and isinstance(bounds, dict):
1965
+ lo, hi = bounds.get("min"), bounds.get("max")
1966
+ if isinstance(lo, list) and isinstance(hi, list) and len(lo) == len(hi) == 3:
1967
+ extents = [abs(float(hi[i]) - float(lo[i])) for i in range(3)]
1968
+ length, width, height = (float(extents[2]), float(extents[0]), float(extents[1])) if isinstance(extents, list) and len(extents) == 3 else (4.4, 1.8, 1.45)
1969
+ wheelbase = 2.62
1970
+ track_front = track_rear = 1.53
1971
+ if all(isinstance(sockets.get(key), list) and len(sockets[key]) == 3 for key in ("wheel_lf", "wheel_rf", "wheel_lr", "wheel_rr")):
1972
+ front_z = (float(sockets["wheel_lf"][2]) + float(sockets["wheel_rf"][2])) / 2
1973
+ rear_z = (float(sockets["wheel_lr"][2]) + float(sockets["wheel_rr"][2])) / 2
1974
+ wheelbase = abs(front_z - rear_z)
1975
+ track_front = abs(float(sockets["wheel_lf"][0]) - float(sockets["wheel_rf"][0]))
1976
+ track_rear = abs(float(sockets["wheel_lr"][0]) - float(sockets["wheel_rr"][0]))
1977
+ prov("physics.chassis.geometry", args.geometry or "HELIX compact-fixture geometry prior", "derived" if args.geometry else "defaulted",
1978
+ "Blender receipt geometry." if args.geometry else "Compact semantic fixture intentionally contains no render geometry.")
1979
+
1980
+ mesh_parts: list[dict[str, Any]] = []
1981
+ part_nodes: dict[str, list[str]] = {}
1982
+ for _, part_name, body, _ in selected:
1983
+ meshes = [str(row.get("mesh")) for row in decode_jbeam_table(body.get("flexbodies")) if row.get("mesh")]
1984
+ if meshes:
1985
+ part_nodes[part_name] = sorted(set(meshes))
1986
+ for node in meshes:
1987
+ role = mesh_role(node)
1988
+ mesh_parts.append({"id": slug(node), "node": node, "role": role, "consumed": role in ("door", "bonnet", "boot", "lamp")})
1989
+ by_id = {row["id"]: row for row in mesh_parts}
1990
+ mesh_parts = [by_id[key] for key in sorted(by_id)]
1991
+
1992
+ panels, panel_evidence = articulation_candidates(selected, selected_slot_rows)
1993
+ for evidence in panel_evidence:
1994
+ panel_id = evidence["panel"]["id"]
1995
+ prov(f"mesh.articulation.panels.{panel_id}", f"selected JBeam part {evidence['rootPart']}", "derived",
1996
+ f"Hinge nodes {', '.join(evidence['hingeNodeIds'])} and {len(evidence['descendantParts'])} "
1997
+ "slot descendants establish rigid visual custody; dynamics remain explicit HELIX priors.")
1998
+
1999
+ lamps: list[dict[str, Any]] = []
2000
+ lamp_colors = {"headlight_low": 0xFFF4D6, "headlight_high": 0xFFFFFF, "tail": 0xFF1808,
2001
+ "brake": 0xFF0000, "reverse": 0xFFFFFF, "indicator_left": 0xFF9B19,
2002
+ "indicator_right": 0xFF9B19, "fog_front": 0xFFF4D6}
2003
+
2004
+ lamp_sources: list[tuple[str, str, dict[str, Any]]] = []
2005
+ for slot in selected_slot_rows:
2006
+ variables = slot.get("variables")
2007
+ channel = variables.get("$electric") if isinstance(variables, dict) else None
2008
+ host = slot.get("hostPart")
2009
+ if isinstance(host, str) and isinstance(channel, str):
2010
+ lamp_sources.append((host, channel, variables))
2011
+ for _, part_name, body, _ in selected:
2012
+ for prop in decode_jbeam_table(body.get("props")):
2013
+ channel = prop.get("$electric") or prop.get("electric")
2014
+ if isinstance(channel, str):
2015
+ lamp_sources.append((part_name, channel, prop))
2016
+
2017
+ for part_name, channel, variables in sorted(lamp_sources, key=lambda row: (row[0], row[1], str(row[2]))):
2018
+ nodes = part_nodes.get(part_name, [])
2019
+ functions = lamp_functions(part_name, channel)
2020
+ if not functions or not nodes:
2021
+ continue
2022
+ for light in functions:
2023
+ side = lamp_side(part_name, channel)
2024
+ semantics = LAMP_NODE_SEMANTICS[light]
2025
+ candidates = [value for value in nodes if any(token in value.lower() for token in semantics)
2026
+ and (not side or side in value.lower())]
2027
+ if not candidates:
2028
+ continue
2029
+ node = sorted(candidates)[0]
2030
+ beam = lamp_beam(light)
2031
+ row = {"id": slug(f"{part_name}-{light}-{channel}"), "light": light, "node": node,
2032
+ "sourceChannel": channel, "colorHex": lamp_colors[light], "intensity": 1,
2033
+ "presentation": "emissive+beam" if beam is not None else "emissive"}
2034
+ if beam is not None:
2035
+ row["beam"] = beam
2036
+ lamps.append(row)
2037
+ lamps.sort(key=lambda row: (row["light"], row["id"]))
2038
+ light_evidence = bool(lamps)
2039
+ if lamps:
2040
+ prov("lights.lamps", "selected JBeam flexbody nodes + $electric channels", "source-literal",
2041
+ "Each HELIX lamp binding is backed by an actual selected node and source electrical channel.")
2042
+
2043
+ sound: dict[str, Any] = {}
2044
+ exhaust: dict[str, Any] = {}
2045
+ for body in engine_node:
2046
+ if isinstance(body.get("soundConfig"), dict): sound.update(body["soundConfig"])
2047
+ if isinstance(body.get("exhaustSoundConfig"), dict): exhaust.update(body["exhaustSoundConfig"])
2048
+ if isinstance(body.get("soundConfigExhaust"), dict): exhaust.update(body["soundConfigExhaust"])
2049
+ engine_sample = sound.get("sampleName")
2050
+ exhaust_sample = exhaust.get("sampleName")
2051
+ if not exhaust_sample:
2052
+ candidate_sample = next((x for x in indexed_values(value_index, "sampleName")
2053
+ if isinstance(x, str) and "exhaust" in x.lower()), None)
2054
+ if candidate_sample: exhaust_sample = candidate_sample
2055
+ engine_override = None
2056
+ if args.engine_override:
2057
+ try:
2058
+ engine_override = json.loads(Path(args.engine_override).read_text())
2059
+ except (OSError, ValueError) as error:
2060
+ fail(f"beamngEngine input is not readable JSON: {error}")
2061
+ if not isinstance(engine_override, dict):
2062
+ fail("beamngEngine input must be a JSON object such as {\"cylinders\": 6}")
2063
+ try:
2064
+ cylinder_evidence = derive_cylinders(
2065
+ [(name, body) for _, name, body, _ in selected], sound, exhaust, engine_override)
2066
+ except ValueError as error:
2067
+ fail(str(error))
2068
+ cylinders = cylinder_evidence["count"]
2069
+ prov("engine.cylinders", cylinder_evidence["source"],
2070
+ "defaulted" if cylinder_evidence["state"] == "defaulted" else
2071
+ "source-literal" if cylinder_evidence["state"] == "declared" else "derived",
2072
+ cylinder_evidence["detail"])
2073
+ print(f"lower-beamng: engine cylinders={cylinders} [{cylinder_evidence['state']}] {cylinder_evidence['detail']}",
2074
+ file=sys.stderr)
2075
+ audio_layers = resolve_layers(source_root, engine_sample, exhaust_sample)
2076
+ audio_bank: dict[str, Any] | None = None
2077
+ audio_unavailable: str | None = None
2078
+ if any(layer["state"] == "shipped" for layer in audio_layers.values()):
2079
+ try:
2080
+ audio_bank = extract_engine_bank(source_root, audio_layers,
2081
+ Path(args.audio_dir) if args.audio_dir else Path(args.output).parent / "audio",
2082
+ locate_ffmpeg())
2083
+ except AudioUnavailable as exc:
2084
+ audio_unavailable = str(exc)
2085
+ engine_sample = engine_sample if isinstance(engine_sample, str) and audio_layers["engine"]["state"] != "absent" else None
2086
+ exhaust_sample = exhaust_sample if isinstance(exhaust_sample, str) and audio_layers["exhaust"]["state"] != "absent" else None
2087
+
2088
+ def layer_reason(layer: str) -> str:
2089
+ state, name = audio_layers[layer]["state"], audio_layers[layer]["name"]
2090
+ if audio_unavailable and state == "shipped":
2091
+ return f"source blend {name} is unusable ({audio_unavailable}); platform fallback engine"
2092
+ if state == "stock":
2093
+ return stock_reason(name)
2094
+ return f"source declares no {layer} sound sample (sampleName {name!r}); platform fallback engine"
2095
+
2096
+ bank_blends = audio_bank["blends"] if audio_bank else {}
2097
+ engine_blend = audio_bank["blendNames"]["engine"] if audio_bank else None
2098
+ exhaust_blend = audio_bank["blendNames"]["exhaust"] if audio_bank else None
2099
+ exhaust_shares_engine = False
2100
+ if audio_bank and engine_blend and audio_layers["exhaust"]["state"] == "absent":
2101
+ # BeamNG mods such as the 993 disable the exhaust layer (sampleName "0") because the single
2102
+ # recording already carries the exhaust; the same set serves both HELIX ladders.
2103
+ exhaust_blend, exhaust_shares_engine = engine_blend, True
2104
+
2105
+ def ladder_note(blend_name: str) -> str:
2106
+ ladders = bank_blends[blend_name]["samples"]
2107
+ rungs = [rung[1] for rung in ladders[0]]
2108
+ return (f"{len(ladders[0])} rungs {min(rungs):g}-{max(rungs):g} rpm from the mod's own recording "
2109
+ f"(sfxBlend2D {blend_name}), {len(ladders)} load row(s).")
2110
+
2111
+ audio_slots = []
2112
+ for slot_name in AUDIO_SLOTS:
2113
+ row: dict[str, Any] = {"slot": slot_name, "buffers": [], "consumed": False,
2114
+ "reason": "No selected source event was mapped to this HELIX slot."}
2115
+ if slot_name in ("engineLadder", "exhaustLadder"):
2116
+ layer = "engine" if slot_name == "engineLadder" else "exhaust"
2117
+ blend_name = engine_blend if layer == "engine" else exhaust_blend
2118
+ sample = engine_sample if layer == "engine" else exhaust_sample
2119
+ if blend_name:
2120
+ note = ladder_note(blend_name)
2121
+ if layer == "exhaust" and exhaust_shares_engine:
2122
+ declared = audio_layers["exhaust"]["name"]
2123
+ note = (f"The source declares no separate exhaust sample (sampleName {declared!r}); the engine "
2124
+ "recording is one combined set used on both ladders. " + note) if declared is not None else \
2125
+ ("The source declares no separate exhaust sample; the engine recording is one combined "
2126
+ "set used on both ladders. " + note)
2127
+ row = {"slot": slot_name, "buffers": [f"blend:{blend_name}"], "consumed": True, "note": note}
2128
+ else:
2129
+ row = {"slot": slot_name, "buffers": [sample] if sample and not audio_bank else [],
2130
+ "consumed": False, "reason": layer_reason(layer)}
2131
+ audio_slots.append(row)
2132
+
2133
+ if audio_bank:
2134
+ bank_source = source_label(Path(audio_layers["engine"]["blend"] if audio_layers["engine"]["state"] == "shipped"
2135
+ else audio_layers["exhaust"]["blend"]))
2136
+ for layer, info in audio_layers.items():
2137
+ if info["state"] == "shipped":
2138
+ prov(f"audio.bank.blends.{info['name']}", source_label(info["blend"]), "source-literal",
2139
+ "rpm ladders copied from the mod's own sfxBlend2D file; rpm values are the source's.")
2140
+ for origin in audio_bank["sources"]:
2141
+ prov(f"audio.bank.assets.{origin['asset']}", source_label(source_root / origin["sourcePath"]),
2142
+ "derived" if origin["transcoded"] else "source-literal",
2143
+ "Recording shipped in the mod; transcoded to 16-bit PCM WAV." if origin["transcoded"]
2144
+ else "Recording shipped in the mod, copied byte for byte.")
2145
+ prov("audio.sampleCylinderCount", bank_source,
2146
+ "defaulted" if cylinder_evidence["state"] == "defaulted" else "derived",
2147
+ "The recorded engine's cylinder count is taken to be the selected engine's: "
2148
+ + cylinder_evidence["detail"])
2149
+ elif audio_unavailable or engine_sample or exhaust_sample:
2150
+ prov("audio.bank", "selected soundConfig sampleName", "unsupported",
2151
+ audio_unavailable or "Source uses BeamNG stock FMOD events; no recording ships in the mod, so the "
2152
+ "platform fallback engine plays.")
2153
+
2154
+ engine_spec = {"torqueCurve": {"points": torque_curve}, "idleRpm": idle, "limiterRpm": limiter,
2155
+ "revLimiterType": "soft", "inertia": inertia, "friction": friction,
2156
+ "dynamicFriction": dyn_friction, "engineBrakeTorque": coast,
2157
+ "burnEfficiency": {"points": [[0, .12], [.05, .27], [.4, .5], [.7, .47], [1, .34]]},
2158
+ "drivetrainEfficiency": .85, "boostTorqueCoef": 0}
2159
+ automatic_shift = transmission_kind in ("dct", "cvt", "automatic")
2160
+ gearbox_spec = {"kind": transmission_kind, "ratios": ratios, "reverseRatio": reverse, "shiftDelay": shift_delay,
2161
+ "inertia": .08, "auto": {"enabled": automatic_shift,
2162
+ "upRpm": max(idle + 1, limiter - 200), "downRpm": 2800}}
2163
+ land = {"version": 1, "driveline": {"engine": engine_spec,
2164
+ "clutch": {"inertia": .1, "capacity": peak_torque * 1.6, "freePlay": .2, "launchStartRpm": 2000, "launchTargetRpm": 2500},
2165
+ "gearbox": gearbox_spec, **differential_runtime},
2166
+ **({"abs": abs_runtime} if abs_runtime is not None else {}),
2167
+ "tyreByAxle": tyre_by_axle}
2168
+
2169
+ physics = {
2170
+ "chassis": {"mass": sourced(float(config_info["Weight"]), "kg", source_ref,
2171
+ "Selected configuration info curb mass." if isinstance(config_info.get("Weight"), (int, float)) else None)
2172
+ if isinstance(config_info.get("Weight"), (int, float)) else sourced(source_mass, "kg", source_ref if source_mass > 100 else None,
2173
+ "Sum of unique selected literal JBeam nodeWeight values; missing common selections remain visible in coverage." if source_mass > 100 else "Explicit fallback pending BeamNG node-mass accumulation."),
2174
+ "weightDistributionFront": sourced(weight_distribution_front, "ratio", note="Explicit sports-sedan prior."), "centreOfMassHeight": sourced(.52, "m", note="Explicit sports-sedan prior."),
2175
+ "wheelbase": sourced(wheelbase, "m", source_ref if sockets else None, "Measured from Blender wheel sockets." if sockets else "Geometry socket derivation unavailable in compact fixture."),
2176
+ "trackFront": sourced(track_front, "m", source_ref if sockets else None, "Measured from Blender wheel sockets." if sockets else "Geometry socket derivation unavailable in compact fixture."),
2177
+ "trackRear": sourced(track_rear, "m", source_ref if sockets else None, "Measured from Blender wheel sockets." if sockets else "Geometry socket derivation unavailable in compact fixture."), "length": sourced(length, "m", None if not args.geometry else source_ref),
2178
+ "width": sourced(width, "m", None if not args.geometry else source_ref), "height": sourced(height, "m", None if not args.geometry else source_ref),
2179
+ "wheelRadius": sourced(wheel_radius, "m", None if not args.geometry else source_ref), "tyreSize": sourced("235/40R18", "text", note="Derived from selected tyre part identity where a numeric tyre row is absent."),
2180
+ "inertiaMultiplier": sourced([1, 1, 1], "ratio", note="Solid-box tensor prior.")},
2181
+ "engine": {"torqueCurve": sourced(torque_curve, "rpm,N.m", source_ref if len(torque) >= 2 else None),
2182
+ "peakPowerKw": sourced(published_power, "kW", source_ref) if published_power is not None else sourced(round(peak_power, 4), "kW", note="maximum(rpm * torque / 9549.296596) from source torque table"),
2183
+ "peakPowerRpm": sourced(published_power_rpm, "rpm", source_ref) if published_power is not None else sourced(peak_power_rpm, "rpm", note="rpm at derived peak power"),
2184
+ "peakTorqueNm": sourced(published_torque, "N.m", source_ref) if published_torque is not None else sourced(peak_torque, "N.m", note="maximum source torque table value"),
2185
+ "peakTorqueRpm": sourced(published_torque_rpm, "rpm", source_ref) if published_torque is not None else sourced(peak_rpm, "rpm", note="rpm at maximum source torque"),
2186
+ "idleRpm": sourced(idle, "rpm", source_ref if idle_key != "HELIX-default" else None), "limiterRpm": sourced(limiter, "rpm", source_ref if limiter_key != "HELIX-default" else None),
2187
+ "drivetrainEfficiency": sourced(.85, "ratio", note="Explicit HELIX driveline runtime prior."), "coastTorque": sourced(coast, "N.m", source_ref),
2188
+ "cylinders": sourced(cylinders, "count", None if cylinder_evidence["state"] in ("defaulted", "operator") else source_ref,
2189
+ cylinder_evidence["detail"]), "strokes": 4,
2190
+ "speedLimiterKph": sourced(None, "km/h", note="No selected literal speed governor.")},
2191
+ **({"induction": induction} if induction else {}),
2192
+ "gearbox": {"ratios": sourced(ratios, "ratio", source_ref if ratios_sourced else None), "reverseRatio": sourced(reverse, "ratio", source_ref if reverse_key != "HELIX-default" else None),
2193
+ "finalDrive": sourced(final_drive, "ratio", source_ref if fd_key != "HELIX-default" else None), "shiftTime": sourced(shift_delay, "s", source_ref if shift_key != "HELIX-default" else None),
2194
+ "autoUpRpm": sourced(max(idle + 1, limiter - 200), "rpm", note="HELIX automatic shift prior; ignored when the selected transmission is manual."), "autoDownRpm": sourced(2800, "rpm", note="HELIX automatic shift prior; ignored when the selected transmission is manual.")},
2195
+ "driveBiasFront": sourced(drive_bias_front, "ratio", source_ref if differentials_functional else None,
2196
+ "Derived from selected driven-axle topology and centre rear torque split."
2197
+ if differentials_functional else
2198
+ "Neutral fallback because selected driveline topology is unsupported."),
2199
+ "brakes": brakes,
2200
+ "aero": {"dragCoefficient": sourced(.32, "ratio", note="Explicit sports-sedan prior."), "frontalArea": sourced(2.2, "m2", note="width*height*shape factor prior."),
2201
+ "airDensity": sourced(1.225, "kg/m3", note="ISA sea-level standard."), "downforceCoefficient": sourced(0, "ratio", note="No selected literal aerodynamic coefficient.")},
2202
+ "grip": {"peak": sourced(1.1, "ratio", note="HELIX tyre-model prior."), "sliding": sourced(.85, "ratio", note="HELIX tyre-model prior."),
2203
+ "lateralMultiplier": sourced(1, "ratio", note="HELIX tyre-model prior."), "biasFront": sourced(.5, "ratio", note="Neutral prior."),
2204
+ "loadSensitivityExponent": sourced(.85, "ratio", note="HELIX tyre-model prior.")},
2205
+ "suspension": suspension,
2206
+ "steering": {"roadWheelLockDeg": sourced(34, "deg", note="Steering-wheel 510 degree source value needs rack ratio; road-wheel prior retained."),
2207
+ "rackTime": sourced(.8, "s", note="Derived from selected steering in/out rate."), "speedSensitivity": sourced([[0, 1], [50, .45]], "m/s,ratio", note="HELIX steering assist prior.")},
2208
+ "linearDamping": sourced(.02, "ratio", note="HELIX solver stability prior."), "angularDamping": sourced(.08, "ratio", note="HELIX solver stability prior.")}
2209
+
2210
+ # Bridge materializes one canonical GLB today. Do not advertise planned
2211
+ # optimization targets as selectable artifacts until the optimizer has
2212
+ # actually sealed them into the package.
2213
+ renditions = [{"id": "source", "kind": "source", "triangleBudget": 500000, "textureMaxDimension": 4096}]
2214
+ tiers = {
2215
+ "minimal": {"renditionId":"source","interior":"hidden","materialLayers":"base","lightBeams":False,"physicalPanels":False,"detachableParts":False},
2216
+ "low": {"renditionId":"source","interior":"simplified","materialLayers":"base","lightBeams":False,"physicalPanels":False,"detachableParts":False},
2217
+ "medium": {"renditionId":"source","interior":"simplified","materialLayers":"dynamic","lightBeams":True,"physicalPanels":True,"detachableParts":False},
2218
+ "high": {"renditionId":"source","interior":"full","materialLayers":"full","lightBeams":True,"physicalPanels":True,"detachableParts":False},
2219
+ "ultra": {"renditionId":"source","interior":"full","materialLayers":"full","lightBeams":True,"physicalPanels":True,"detachableParts":False},
2220
+ }
2221
+ package_id = f"beamng-{slug(candidate)}-{slug(config_path.stem)}"
2222
+ package_uuid = stable_uuid(f"vehicle-package:{plan_key}:{source_sha}:{candidate}:{config_path.name}")
2223
+ top_speed_raw = config_info.get("Top Speed")
2224
+ top_speed_kph = float(top_speed_raw) * 3.6 if isinstance(top_speed_raw, (int, float)) and top_speed_raw < 120 else top_speed_raw
2225
+ braking_raw = config_info.get("60-0 mph")
2226
+ braking_m = float(braking_raw) * .3048 if isinstance(braking_raw, (int, float)) and braking_raw > 80 else braking_raw
2227
+ target_specs = {
2228
+ "zeroToSixtyMphS": (config_info.get("0-60 mph"), 7.0, "s"),
2229
+ "zeroToHundredKphS": (config_info.get("0-100 km/h"), 7.5, "s"),
2230
+ "quarterMileS": (config_info.get("Drag Times", {}).get("time_1_4") if isinstance(config_info.get("Drag Times"), dict) else None, 15.0, "s"),
2231
+ "topSpeedKph": (top_speed_kph, 220, "km/h"), "ungovernedTopSpeedKph": (top_speed_kph, 220, "km/h"),
2232
+ "lateralG": (None, 1.0, "g"), "brakingSixtyToZeroM": (braking_m, 36, "m"),
2233
+ }
2234
+ vehicle_package = {"schemaVersion": "helix.vehicle-package/2", "packageVersion": 2, "id": package_id,
2235
+ "displayName": f"{candidate} / {config_path.stem}", "years": "source-defined", "provenance": {"distributable": False,
2236
+ "note": "Source-local BeamNG lowering. Publication remains gated until imported assets and source policy are resolved."},
2237
+ "citations": [{"id": citation_id, "title": f"BeamNG source closure for {candidate}/{config_path.name}", "publisher": "BeamNG source import",
2238
+ "kind": "measured-fixture", "note": f"bridgePlanKey={plan_key}; sourceSha256={source_sha}"}],
2239
+ "mesh": {"source": "fixture", "ref": f"{candidate}.glb", "classId": "car", "sockets": sockets, "wheelVisuals": "author-static",
2240
+ "parts": mesh_parts, "visual": {"format": 1, "renditions": renditions, "tiers": tiers},
2241
+ "articulation": {"format": 1, "panels": panels}},
2242
+ "physics": physics, "simulation": {"format": 1, "fixedStepHz": 60, "qualityInvariant": True, "land": land},
2243
+ "targets": {name: (sourced(float(raw), unit, source_ref) if isinstance(raw, (int, float))
2244
+ else sourced(fallback, unit, note="Provisional functional acceptance target; no BeamNG literal supplied.")) | {"tolerance": .35}
2245
+ for name, (raw, fallback, unit) in target_specs.items()},
2246
+ "audio": {"bankId": package_id, "sampleCylinderCount": cylinders, "strokes": 4,
2247
+ **({"engineBlend": engine_blend} if engine_blend else {"engineBlend": engine_sample} if engine_sample and not audio_bank else {}),
2248
+ **({"exhaustBlend": exhaust_blend} if exhaust_blend else {"exhaustBlend": exhaust_sample} if exhaust_sample and not audio_bank else {}),
2249
+ **({"bank": {"blends": audio_bank["blends"], "assets": audio_bank["assets"]}} if audio_bank else {}),
2250
+ "slots": audio_slots, "engineGainDb": float(sound.get("mainGain", 0) or 0), "exhaustGainDb": float(exhaust.get("mainGain", 0) or 0),
2251
+ "minLoadMix": float(sound.get("minLoadMix", 0) or 0), "maxLoadMix": float(sound.get("maxLoadMix", 1) or 1), "muffling": float(sound.get("intakeMuffling", 0) or 0)},
2252
+ "lights": {"lamps": lamps, "indicatorHz": 1.5, "decelBrakeLightThreshold": 4},
2253
+ "vfx": [{"slot": name, "anchors": ["exhaust"] if name == "exhaustSmoke" else [], "consumed": False,
2254
+ "reason": "Source event retained; no portable BeamNG particle asset is lowered."}
2255
+ for name in ("exhaustSmoke", "tyreMarks", "tyreSmoke", "backfireFlame", "brakeGlow", "dust")]}
2256
+
2257
+ addon_records = []
2258
+ add_on_selections = sorted({(row["slotType"], row["partName"]) for row in closure_rows if row["slotType"] != "$main"})
2259
+ for slot_name, part_name in add_on_selections:
2260
+ found = definitions.get(part_name)
2261
+ nodes = part_nodes.get(part_name, [])
2262
+ mapped_slot = canonical_addon_slot(slot_name, candidate)
2263
+ schema_compatible = mapped_slot is not None
2264
+ runtime_slot = mapped_slot or slot_name
2265
+ add_on = {"id": stable_uuid(f"addon:{plan_key}:{candidate}:{slot_name}:{part_name}"), "kind": "add_on", "version": "1",
2266
+ "fits": {"slotType": runtime_slot, "requires": {"sockets": [], "paths": [], "pointers": [], "nodes": [], "skeleton": None}},
2267
+ "ops": [{"v": "mount", "row": {"type": "articulator", "id": slug(part_name),
2268
+ "sourcePart": part_name, "sourceSlotType": slot_name, "nodes": nodes}}]}
2269
+ executable_visual = bool(schema_compatible and found and nodes)
2270
+ addon_records.append({"slotType": runtime_slot, "sourceSlotType": slot_name, "partName": part_name, "source": source_label(found[1]) if found else f"{config_path.name}#parts.{slot_name}",
2271
+ "schemaCompatible": schema_compatible, "runtimeCompatible": executable_visual,
2272
+ "compatibilityScope": "visual" if executable_visual else "source-record-only", "package": add_on,
2273
+ "provenance": {"disposition": "source-literal" if found else "missing-source",
2274
+ "note": "Stable source selection identity. runtimeCompatible means this record carries selected visual nodes in a known HELIX slot; unknown or base-build-only source parts remain explicit but are not claimed equip-ready."}})
2275
+
2276
+ build = {"schemaVersion": "helix.vehicle-build-source/1", "id": stable_uuid(f"build:{plan_key}:{config_sha}"),
2277
+ "vehiclePackageId": package_uuid, "vehiclePackageSourceId": package_id,
2278
+ "configuration": {"file": config_path.name, "sha256": config_sha, "format": 2},
2279
+ "selections": [{"slotType": slot_name, "partName": part_name,
2280
+ "addOnPackageId": stable_uuid(f"addon:{plan_key}:{candidate}:{slot_name}:{part_name}"),
2281
+ "resolution": "resolved" if part_name in definitions else "missing-source"}
2282
+ for slot_name, part_name in explicit],
2283
+ "resolvedClosure": closure_rows,
2284
+ "continuumLowering": {"status": "requires-publication-refs", "consumer": "createPrivateBuildDraft -> freezeBuildDraft",
2285
+ "note": "Publisher replaces source identities with immutable Continuum Package/CID refs; this compiler never invents them."}}
2286
+
2287
+ controllers = sorted({str(row.get("fileName")) for _, _, body, _ in selected
2288
+ for row in decode_jbeam_table(body.get("controller")) if row.get("fileName")})
2289
+ source_semantics = {
2290
+ "mass": {"uniqueNodes": len(node_masses), "selectedNodeMassKg": source_mass},
2291
+ "seats": sorted(name for name in selected_names if "seat" in name.lower()),
2292
+ "cameras": [{"partName": name, "section": section, "rows": decode_jbeam_table(body.get(section))}
2293
+ for _, name, body, _ in selected for section in ("cameraChase", "cameraExternal", "camerasInternal")
2294
+ if isinstance(body.get(section), list)],
2295
+ "brakes": {"selectedParts": sorted(name for name in selected_names if "brake" in name.lower()),
2296
+ "sourceExpressions": sorted({str(row.get("brakeTorque", {}).get("raw"))
2297
+ for row in brake_semantics["axles"].values()
2298
+ if isinstance(row, dict) and isinstance(row.get("brakeTorque"), dict)
2299
+ and row["brakeTorque"].get("raw") is not None}),
2300
+ **brake_semantics},
2301
+ "suspension": suspension_semantics,
2302
+ "driveline": driveline_semantics,
2303
+ "wheelsTyres": {"selectedParts": sorted(name for name in selected_names if any(token in name.lower() for token in ("wheel", "rim", "tire", "tyre"))),
2304
+ **tyre_semantics},
2305
+ "electrics": sorted({str(value) for value in indexed_values(value_index, "electricsName") + indexed_values(value_index, "$electric")}),
2306
+ "audioEvents": sorted({str(value) for value in indexed_values(value_index, "sampleName") + indexed_values(value_index, "deformSound")}),
2307
+ "openableControllers": [name for name in controllers if name == "advancedCouplerControl"],
2308
+ "controllers": {"abs": abs_semantics},
2309
+ }
2310
+ abs_capability_source = [
2311
+ abs_semantics["activation"]["source"],
2312
+ f"{abs_semantics['sourceModule']['path']}@sha256:{abs_semantics['sourceModule']['sha256']}",
2313
+ ] if abs_adapted else [
2314
+ row.get("source", f"{row.get('partName', 'unknown')}#{row.get('field', 'unknown')}")
2315
+ for row in abs_semantics.get("activationCandidates", [])
2316
+ ] or ["selected JBeam ABS controller shape"]
2317
+ capabilities = [
2318
+ {"capability": "beamng.controller.abs", "disposition": "derived" if abs_adapted else "unsupported",
2319
+ "source": abs_capability_source,
2320
+ "note": ("One fingerprinted BeamNG wheel-core ABS shape is statically projected into HELIX AbsSpec; raw Lua is never executed."
2321
+ if abs_adapted else abs_semantics["reason"])},
2322
+ {"capability": "beamng.controller", "disposition": "unsupported", "source": controllers or ["selected JBeam controller tables"],
2323
+ "note": "Controller declarations outside explicitly versioned static adapters remain unsupported and auditable."},
2324
+ {"capability": "beamng.lua", "disposition": "unsupported", "source": sorted(lua_files) or ["source closure scan: no Lua file selected"],
2325
+ "note": "Raw BeamNG Lua is never executed or translated."},
2326
+ {"capability": "softbody", "disposition": "out-of-scope", "source": ["selected JBeam nodes/beams/triangles"],
2327
+ "note": "Rigid mass, sockets, tyres, and articulation are lowered; deformation is not."},
2328
+ {"capability": "fluids", "disposition": "out-of-scope", "source": ["selected JBeam energy and thermal declarations"],
2329
+ "note": "Fuel/oil/coolant simulation is intentionally outside this contract."},
2330
+ {"capability": "detailed-damage", "disposition": "out-of-scope", "source": ["selected JBeam break/deform groups"],
2331
+ "note": "Hinge break groups are retained only as future detachment eligibility; no production tier enables detachment."},
2332
+ ]
2333
+ prov("runtime.controllers", "selected JBeam controller tables", "unsupported",
2334
+ f"{len(controllers)} controller types retained in capability ledger; the ABS subset is governed by its separate static adapter receipt.")
2335
+ prov("runtime.softbody", "selected JBeam nodes/beams/triangles", "out-of-scope", "Not emitted into runtime contract.")
2336
+ prov("runtime.fluids", "selected JBeam energy/thermal sections", "out-of-scope", "Not emitted into runtime contract.")
2337
+ prov("mesh.visual.tiers", "HELIX engine fidelity contract", "defaulted", "Presentation-only policy; simulation and identity are invariant.")
2338
+
2339
+ provenance.sort(key=lambda row: (row["target"], row["source"], row["disposition"], row["detail"]))
2340
+ literal_count = sum(row["disposition"] == "source-literal" for row in provenance)
2341
+ resolved_count = sum(part_name in definitions for _, part_name in explicit)
2342
+ turbo_declared = "turbo" in str(config_info.get("Induction Type", "")).lower()
2343
+ future_detachable_panels = sorted(evidence["panel"]["id"] for evidence in panel_evidence
2344
+ if evidence["hingeBreakGroups"])
2345
+ functional_domains = {
2346
+ "geometrySockets": bool(sockets),
2347
+ "engine": bool(engine_parts and len(torque) >= 2 and (not turbo_declared or induction is not None)),
2348
+ "gearbox": ratios_sourced and transmission_kind_sourced,
2349
+ "differentials": differentials_functional,
2350
+ "brakes": brakes_functional,
2351
+ "abs": abs_adapted and abs_semantics.get("parity") == "functional",
2352
+ "suspension": False,
2353
+ "wheelsTyres": tyres_functional,
2354
+ "seatsCameras": any(str(name).startswith("seat") for name in sockets),
2355
+ "lightsElectrics": light_evidence,
2356
+ "audio": bool(engine_sample or exhaust_sample),
2357
+ "rigidArticulation": bool(panels),
2358
+ "rigidDetachment": False,
2359
+ }
2360
+ output = {"schemaVersion": SCHEMA,
2361
+ "identity": {"bridgePlanKey": plan_key, "sourceSha256": source_sha, "candidateId": candidate,
2362
+ "configurationSha256": config_sha, "vehiclePackageId": package_uuid},
2363
+ "vehiclePackage": vehicle_package,
2364
+ "addOnPackages": {"schemaVersion": "helix.vehicle-addon-packages/1", "records": addon_records},
2365
+ "build": build, "sourceSemantics": source_semantics, "capabilities": capabilities, "provenance": provenance,
2366
+ "coverage": {"explicitSelections": len(explicit), "resolvedSelections": resolved_count,
2367
+ "missingExplicitSelections": len(explicit) - resolved_count,
2368
+ "resolvedClosureParts": len(selected), "missingSelections": len(missing),
2369
+ "resolvedSelectionPercent": round(100 * resolved_count / max(1, len(explicit)), 2),
2370
+ "resolvedClosurePercent": round(100 * len(selected) / max(1, len(selected) + len(missing)), 2),
2371
+ "sourceLiteralPercent": round(100 * literal_count / max(1, len(provenance)), 2),
2372
+ "suspensionProvenance": {
2373
+ disposition: sum(row["disposition"] == disposition
2374
+ and (row["target"].startswith("physics.suspension")
2375
+ or row["target"].startswith("sourceSemantics.suspension"))
2376
+ for row in provenance)
2377
+ for disposition in ("source-literal", "derived", "defaulted", "unsupported")
2378
+ },
2379
+ "functionalDomains": functional_domains,
2380
+ "futureEligibility": {"rigidDetachment": {
2381
+ "eligiblePanelIds": future_detachable_panels,
2382
+ "productionEnabled": False,
2383
+ }},
2384
+ "jbeamParse": {"mode": "selected-part-index" if args.part_index else "fallback-cache",
2385
+ "parsedFiles": parsed_files, "indexedFiles": indexed_files,
2386
+ "producerParsedFiles": producer_parsed_files,
2387
+ "maximumParsesPerFile": maximum_parses_per_file},
2388
+ "parseFailures": parse_failures}}
2389
+ out = Path(args.output)
2390
+ out.parent.mkdir(parents=True, exist_ok=True)
2391
+ out.write_text(json.dumps(output, indent=2, sort_keys=True, separators=(",", ": ")) + "\n")
2392
+
2393
+
2394
+ if __name__ == "__main__":
2395
+ try:
2396
+ main()
2397
+ except Exception as exc:
2398
+ print(f"BeamNG functional lowering refused: {exc}", file=sys.stderr)
2399
+ raise SystemExit(2)