@hypersoniclabs/helix-cli 0.1.7 → 0.1.24-helix3.438
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.
- package/README.md +631 -8
- package/assets/bridge/metahuman-eye-baked.png +0 -0
- package/assets/item-def-sounds/Common/A_DryShot.ogg +0 -0
- package/assets/item-def-sounds/Common/A_LMG_CloseCoverA.ogg +0 -0
- package/assets/item-def-sounds/Common/A_LMG_CloseCoverB.ogg +0 -0
- package/assets/item-def-sounds/Common/A_LMG_InsertMag.ogg +0 -0
- package/assets/item-def-sounds/Common/A_LMG_RemoveMag.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Pistol_InsertMag_001.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Pistol_InsertMag_002.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Pistol_InsertMag_003.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Pistol_RemoveMag_001.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Pistol_RemoveMag_002.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Pistol_RemoveMag_003.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_ChargingHandle_001.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_ChargingHandle_002.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_ChargingHandle_003.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_InsertMag_001.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_InsertMag_002.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_InsertMag_003.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_RemoveMag_001.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_RemoveMag_002.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Rifle_RemoveMag_003.ogg +0 -0
- package/assets/item-def-sounds/Common/A_SMG_ChargingHandle_001.ogg +0 -0
- package/assets/item-def-sounds/Common/A_SMG_ChargingHandle_002.ogg +0 -0
- package/assets/item-def-sounds/Common/A_SMG_ChargingHandle_003.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Sniper_ChargingHandle.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Sniper_RemoveMag_001.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Sniper_RemoveMag_002.ogg +0 -0
- package/assets/item-def-sounds/Common/A_Sniper_RemoveMag_003.ogg +0 -0
- package/assets/item-def-sounds/Explosions/Explosions_Grenade_Punch-Close_01.ogg +0 -0
- package/assets/item-def-sounds/Explosions/Explosions_Grenade_Punch-Close_02.ogg +0 -0
- package/assets/item-def-sounds/Explosions/Explosions_Grenade_Punch-Close_03.ogg +0 -0
- package/assets/item-def-sounds/LMG/A_LWS-32_Shot_001.ogg +0 -0
- package/assets/item-def-sounds/LMG/A_LWS-32_Shot_002.ogg +0 -0
- package/assets/item-def-sounds/LMG/A_LWS-32_Shot_003.ogg +0 -0
- package/assets/item-def-sounds/Pistol/A_Gaston_Shot_001.ogg +0 -0
- package/assets/item-def-sounds/Pistol/A_Gaston_Shot_002.ogg +0 -0
- package/assets/item-def-sounds/Pistol/A_Gaston_Shot_003.ogg +0 -0
- package/assets/item-def-sounds/Rifle/A_KAL_Shot_001.ogg +0 -0
- package/assets/item-def-sounds/Rifle/A_KAL_Shot_002.ogg +0 -0
- package/assets/item-def-sounds/Rifle/A_KAL_Shot_003.ogg +0 -0
- package/assets/item-def-sounds/SMG/A_Bison_InsertMag.ogg +0 -0
- package/assets/item-def-sounds/SMG/A_Bison_RemoveMag.ogg +0 -0
- package/assets/item-def-sounds/SMG/A_Bison_Shot_001.ogg +0 -0
- package/assets/item-def-sounds/SMG/A_Bison_Shot_002.ogg +0 -0
- package/assets/item-def-sounds/SMG/A_Bison_Shot_003.ogg +0 -0
- package/assets/item-def-sounds/Shotgun/A_DB-12_Shot_001.ogg +0 -0
- package/assets/item-def-sounds/Shotgun/A_DB-12_Shot_002.ogg +0 -0
- package/assets/item-def-sounds/Shotgun/A_DB-12_Shot_003.ogg +0 -0
- package/assets/item-def-sounds/Sniper/A_CS-446_Shot_001.ogg +0 -0
- package/assets/item-def-sounds/Sniper/A_CS-446_Shot_002.ogg +0 -0
- package/assets/item-def-sounds/Sniper/A_CS-446_Shot_003.ogg +0 -0
- package/bridge-plugins/helix.bridge.unreal/bin/unreal-skeletal-mesh-extract.js +812 -0
- package/bridge-plugins/helix.bridge.unreal/bridge-plugin.json +40 -0
- package/bridge-plugins/helix.bridge.unreal/test/exporter.test.js +418 -0
- package/bridge-plugins/helix.bridge.unreal/test/handler.test.js +417 -0
- package/bridge-plugins/helix.bridge.unreal/unreal/export_skeletal_mesh.py +384 -0
- package/bridge-recipes/cyberpunk/cyberpunk-female-bone-map.json +476 -0
- package/bridge-recipes/sims4/README.md +43 -0
- package/bridge-recipes/sims4/sims4-female-bone-map.json +65 -0
- package/bridge-recipes/unreal/character-customizer/README.md +58 -0
- package/bridge-recipes/unreal/character-customizer/all.json +13 -0
- package/bridge-recipes/unreal/character-customizer/base-male-body.json +6 -0
- package/bridge-recipes/unreal/character-customizer/base-male-head.json +6 -0
- package/bridge-recipes/unreal/character-customizer/beard.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/beard.json +6 -0
- package/bridge-recipes/unreal/character-customizer/bottom.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/bottom.json +6 -0
- package/bridge-recipes/unreal/character-customizer/eyewear.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/eyewear.json +6 -0
- package/bridge-recipes/unreal/character-customizer/footwear.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/footwear.json +6 -0
- package/bridge-recipes/unreal/character-customizer/hair.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/hair.json +6 -0
- package/bridge-recipes/unreal/character-customizer/test/recipes.test.js +74 -0
- package/bridge-recipes/unreal/character-customizer/top.addon.json +1 -0
- package/bridge-recipes/unreal/character-customizer/top.json +6 -0
- package/bridge-recipes/unreal/helix-character-creator-female/README.md +57 -0
- package/bridge-recipes/unreal/helix-character-creator-female/all.json +18 -0
- package/bridge-recipes/unreal/helix-character-creator-female/back.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/back.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/base-female-face.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/base-female.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/bottom.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/bottom.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/eyewear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/eyewear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-basic-t-shirt.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-basic-t-shirt.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-batch-hair.json +23 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-batch-nebula-urbanghost.json +7 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-batch-singles.json +19 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-batch5.json +19 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-batch6.json +19 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-batch7.json +11 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-cap-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-cap-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-cap.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-cap.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-cargo-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-cargo-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-carnival-mask-3.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-carnival-mask-3.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-chain.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-chain.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-clare-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-clare-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-clare-shoes.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-clare-shoes.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-clare-top.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-clare-top.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-compression-bodysuit-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-compression-bodysuit-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-compression-bodysuit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-compression-bodysuit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-eye-hole-mask-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-eye-hole-mask-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-eye-patch.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-eye-patch.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-firefight-outfit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-firefight-outfit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-sneakers.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-sneakers.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-top.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-fruity-top.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-gamer-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-gamer-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-guy-fawkes-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-guy-fawkes-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a1.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a1.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a10.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a10.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a11.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a11.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a12.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a12.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a13.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a13.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a14.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a14.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a15.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a15.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a16.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a16.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a17.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a17.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a18.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a18.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a19.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a19.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a20.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a20.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a21.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a21.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a3.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a3.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a4.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a4.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a5.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a5.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a6.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a6.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a7.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a7.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a8.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a8.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a9.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hair-a9.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-high-waist-tailored-trousers.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-high-waist-tailored-trousers.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hogwarts-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hogwarts-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hoody.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-hoody.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-jacket-6.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-jacket-6.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-joker-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-joker-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-kombes-brown.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-kombes-brown.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-kombes.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-kombes.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-marshmallow-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-marshmallow-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-maska.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-maska.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-nebula-executive-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-nebula-executive-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-nebula-executive-shoes.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-nebula-executive-shoes.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-ninza-costume.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-ninza-costume.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-orange-kombes.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-orange-kombes.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-panama.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-panama.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-panda-bag-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-panda-bag-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-pantslow.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-pantslow.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-paper-bag-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-paper-bag-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-pig-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-pig-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-police-uniform.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-police-uniform.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-prison-outfit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-prison-outfit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-short-b4low.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-short-b4low.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-short-swimming.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-short-swimming.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-shortlow.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-shortlow.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-shorts-b3.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-shorts-b3.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-sterile-gloves.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-sterile-gloves.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-surgeon-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-surgeon-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-swat.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-swat.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-top.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-top.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-topx1.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-topx1.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-hoodie.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-hoodie.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-urban-ghost-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-warehouse-uniform.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-warehouse-uniform.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-wide-top.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-wide-top.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-wolf-set.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-wolf-set.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-woman-mask-3.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-woman-mask-3.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-women-out-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/female-women-out-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/footwear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/footwear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/gloves.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/gloves.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/hair.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/hair.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/headwear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/headwear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/neck.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/neck.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/outfit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/outfit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/top.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-female/top.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/README.md +15 -0
- package/bridge-recipes/unreal/helix-character-creator-male/all.json +18 -0
- package/bridge-recipes/unreal/helix-character-creator-male/back.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/back.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/base-male-face.json +6 -0
- package/bridge-recipes/unreal/helix-character-creator-male/base-male.json +6 -0
- package/bridge-recipes/unreal/helix-character-creator-male/bottom.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/bottom.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/eyewear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/eyewear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/footwear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/footwear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/gloves.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/gloves.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/hair.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/hair.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/headwear.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/headwear.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-batch-hair.json +23 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-batch-nebula-urbanghost.json +7 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-batch-uniforms.json +10 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-batch6-slot2.json +7 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-batch7-slot3.json +6 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-batch8-combined.json +11 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-boots-25.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-cargo-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-cargo-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-carnival-mask-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-carnival-mask-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-carnival-mask-3.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-carnival-mask-3.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-clare-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-clare-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-clare-shoes.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-clare-shoes.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-clare-top.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-clare-top.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-diamond-hands.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-diamond-hands.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-doctor-outfit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-doctor-outfit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-eye-hole-mask-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-eye-hole-mask-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-eye-hole-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-eye-hole-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-eye-patch.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-eye-patch.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-fb-cargo-pants-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-fb-cargo-pants-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-fb-cargo-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-fb-puffy.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-fb-puffy.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-firefight-outfit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-firefight-outfit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-gamer-mask-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-gamer-mask-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a1.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a1.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a10.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a10.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a11.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a11.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a12.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a12.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a13.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a13.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a14.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a14.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a15.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a15.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a16.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a16.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a17.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a17.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a18.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a18.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a19.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a19.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a20.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a20.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a21.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a21.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a3.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a3.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a4.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a4.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a5.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a5.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a6.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a6.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a7.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a7.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a8.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a8.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a9.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-hair-a9.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-kombes-brown-v2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-kombes-brown-v2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-kombes-brown.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-man-mask-3.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-marshmallow-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-marshmallow-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-mikejack-jacket.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-coat.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-coat.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-shoes.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-nebula-executive-shoes.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-gloves.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-gloves.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-mantle.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-mantle.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-tunic.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-neo-arcane-heir-tunic.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-orangekombes.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-orangekombes.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-out2-boots.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-out2-boots.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-panda-bag-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-panda-bag-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-pants-2.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-pants-2.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-paper-bag-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-paper-bag-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-pig-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-pig-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-police-uniform.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-police-uniform.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-prison-outfit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-prison-outfit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-puffer-jacket.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-puffer-jacket.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-pullover-hoodie.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-pullover-hoodie.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-robber-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-robber-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-short-swimming.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-short-swimming.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-sinth-ascetic-jacket.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-sinth-ascetic-jacket.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-sip-cap.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-sip-cap.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-socks.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-socks.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-surgeon-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-surgeon-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-tech-hoodie.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-tech-hoodie.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-hoodie.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-hoodie.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-pants.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-urban-ghost-pants.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-warehouse-uniform.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/male-warehouse-uniform.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/mask.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/mask.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/neck.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/neck.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/outfit.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/outfit.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/palette-pilot.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/test/recipes.test.js +122 -0
- package/bridge-recipes/unreal/helix-character-creator-male/top.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/top.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/variants/bottom-nopalette.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/variants/headwear-nopalette.addon.json +1 -0
- package/bridge-recipes/unreal/helix-character-creator-male/variants/neck-nopalette.addon.json +1 -0
- package/dist/ability.d.ts +2 -2
- package/dist/ability.js +6 -5
- package/dist/ability.js.map +1 -1
- package/dist/achievement.d.ts +123 -0
- package/dist/achievement.js +400 -0
- package/dist/achievement.js.map +1 -0
- package/dist/api.d.ts +19 -1
- package/dist/api.js +80 -6
- package/dist/api.js.map +1 -1
- package/dist/assets.d.ts +595 -0
- package/dist/assets.js +1121 -0
- package/dist/assets.js.map +1 -0
- package/dist/audioCommand.d.ts +65 -0
- package/dist/audioCommand.js +181 -0
- package/dist/audioCommand.js.map +1 -0
- package/dist/bakeBodyRegionsCli.d.ts +19 -0
- package/dist/bakeBodyRegionsCli.js +25 -0
- package/dist/bakeBodyRegionsCli.js.map +1 -0
- package/dist/bakeLighting.d.ts +173 -0
- package/dist/bakeLighting.js +452 -0
- package/dist/bakeLighting.js.map +1 -0
- package/dist/bridge/bridge.testutil.d.ts +105 -0
- package/dist/bridge/bridge.testutil.js +259 -0
- package/dist/bridge/bridge.testutil.js.map +1 -0
- package/dist/bridge/characterAddon.d.ts +47 -0
- package/dist/bridge/characterAddon.js +146 -0
- package/dist/bridge/characterAddon.js.map +1 -0
- package/dist/bridge/detect.d.ts +4 -0
- package/dist/bridge/detect.js +154 -0
- package/dist/bridge/detect.js.map +1 -0
- package/dist/bridge/execute.d.ts +3 -0
- package/dist/bridge/execute.js +206 -0
- package/dist/bridge/execute.js.map +1 -0
- package/dist/bridge/glb.d.ts +60 -0
- package/dist/bridge/glb.js +81 -0
- package/dist/bridge/glb.js.map +1 -0
- package/dist/bridge/hash.d.ts +6 -0
- package/dist/bridge/hash.js +40 -0
- package/dist/bridge/hash.js.map +1 -0
- package/dist/bridge/index.d.ts +11 -0
- package/dist/bridge/index.js +28 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/bridge/internal/artifacts.d.ts +5 -0
- package/dist/bridge/internal/artifacts.js +31 -0
- package/dist/bridge/internal/artifacts.js.map +1 -0
- package/dist/bridge/internal/builtInExecution.d.ts +4 -0
- package/dist/bridge/internal/builtInExecution.js +514 -0
- package/dist/bridge/internal/builtInExecution.js.map +1 -0
- package/dist/bridge/internal/externalExecution.d.ts +12 -0
- package/dist/bridge/internal/externalExecution.js +261 -0
- package/dist/bridge/internal/externalExecution.js.map +1 -0
- package/dist/bridge/internal/outputReservation.d.ts +13 -0
- package/dist/bridge/internal/outputReservation.js +151 -0
- package/dist/bridge/internal/outputReservation.js.map +1 -0
- package/dist/bridge/internal/snapshots.d.ts +32 -0
- package/dist/bridge/internal/snapshots.js +562 -0
- package/dist/bridge/internal/snapshots.js.map +1 -0
- package/dist/bridge/management.d.ts +40 -0
- package/dist/bridge/management.js +333 -0
- package/dist/bridge/management.js.map +1 -0
- package/dist/bridge/manifest.d.ts +8 -0
- package/dist/bridge/manifest.js +192 -0
- package/dist/bridge/manifest.js.map +1 -0
- package/dist/bridge/metahuman.d.ts +26 -0
- package/dist/bridge/metahuman.js +332 -0
- package/dist/bridge/metahuman.js.map +1 -0
- package/dist/bridge/planner.d.ts +9 -0
- package/dist/bridge/planner.js +246 -0
- package/dist/bridge/planner.js.map +1 -0
- package/dist/bridge/plugins.d.ts +17 -0
- package/dist/bridge/plugins.js +307 -0
- package/dist/bridge/plugins.js.map +1 -0
- package/dist/bridge/resolution.d.ts +8 -0
- package/dist/bridge/resolution.js +55 -0
- package/dist/bridge/resolution.js.map +1 -0
- package/dist/bridge/types.d.ts +247 -0
- package/dist/bridge/types.js +60 -0
- package/dist/bridge/types.js.map +1 -0
- package/dist/bridge/validate.d.ts +12 -0
- package/dist/bridge/validate.js +370 -0
- package/dist/bridge/validate.js.map +1 -0
- package/dist/bridge/zipSource.d.ts +27 -0
- package/dist/bridge/zipSource.js +90 -0
- package/dist/bridge/zipSource.js.map +1 -0
- package/dist/browser/hostSlots.d.ts +57 -0
- package/dist/browser/hostSlots.js +371 -0
- package/dist/browser/hostSlots.js.map +1 -0
- package/dist/browser/launch.d.ts +25 -0
- package/dist/browser/launch.js +74 -0
- package/dist/browser/launch.js.map +1 -0
- package/dist/browser/nonSystemChrome.d.ts +15 -0
- package/dist/browser/nonSystemChrome.js +91 -0
- package/dist/browser/nonSystemChrome.js.map +1 -0
- package/dist/builds/index.d.ts +54 -0
- package/dist/builds/index.js +215 -0
- package/dist/builds/index.js.map +1 -0
- package/dist/bundle.d.ts +35 -2
- package/dist/bundle.js +437 -20
- package/dist/bundle.js.map +1 -1
- package/dist/canary/cli.d.ts +17 -0
- package/dist/canary/cli.js +95 -0
- package/dist/canary/cli.js.map +1 -0
- package/dist/canary/index.d.ts +4 -0
- package/dist/canary/index.js +21 -0
- package/dist/canary/index.js.map +1 -0
- package/dist/canary/inventory.d.ts +46 -0
- package/dist/canary/inventory.js +499 -0
- package/dist/canary/inventory.js.map +1 -0
- package/dist/canary/pacifica.d.ts +51 -0
- package/dist/canary/pacifica.js +247 -0
- package/dist/canary/pacifica.js.map +1 -0
- package/dist/canary/plan.d.ts +115 -0
- package/dist/canary/plan.js +479 -0
- package/dist/canary/plan.js.map +1 -0
- package/dist/character/addonPushOut.d.ts +66 -0
- package/dist/character/addonPushOut.js +211 -0
- package/dist/character/addonPushOut.js.map +1 -0
- package/dist/character/avatarPreflight.d.ts +92 -0
- package/dist/character/avatarPreflight.js +261 -0
- package/dist/character/avatarPreflight.js.map +1 -0
- package/dist/character/bakeBodyRegions.d.ts +80 -0
- package/dist/character/bakeBodyRegions.js +190 -0
- package/dist/character/bakeBodyRegions.js.map +1 -0
- package/dist/character/baseColorGuard.d.ts +62 -0
- package/dist/character/baseColorGuard.js +141 -0
- package/dist/character/baseColorGuard.js.map +1 -0
- package/dist/character/bodyGeometryGuard.d.ts +243 -0
- package/dist/character/bodyGeometryGuard.js +417 -0
- package/dist/character/bodyGeometryGuard.js.map +1 -0
- package/dist/character/bodyRegionHide.d.ts +57 -0
- package/dist/character/bodyRegionHide.js +97 -0
- package/dist/character/bodyRegionHide.js.map +1 -0
- package/dist/character/composeBaseMale.d.ts +25 -0
- package/dist/character/composeBaseMale.js +712 -0
- package/dist/character/composeBaseMale.js.map +1 -0
- package/dist/character/esm.js +3 -1
- package/dist/character/esm.js.map +1 -1
- package/dist/character/faceManifest.d.ts +53 -0
- package/dist/character/faceManifest.js +256 -0
- package/dist/character/faceManifest.js.map +1 -0
- package/dist/character/fbx.d.ts +70 -0
- package/dist/character/fbx.js +396 -0
- package/dist/character/fbx.js.map +1 -0
- package/dist/character/fitGarmentFile.d.ts +39 -0
- package/dist/character/fitGarmentFile.js +53 -0
- package/dist/character/fitGarmentFile.js.map +1 -0
- package/dist/character/helixPalette.d.ts +68 -0
- package/dist/character/helixPalette.js +183 -0
- package/dist/character/helixPalette.js.map +1 -0
- package/dist/character/hideCoverage.d.ts +135 -0
- package/dist/character/hideCoverage.js +450 -0
- package/dist/character/hideCoverage.js.map +1 -0
- package/dist/character/imageLimits.d.ts +3 -0
- package/dist/character/imageLimits.js +7 -0
- package/dist/character/imageLimits.js.map +1 -0
- package/dist/character/importCharacter.d.ts +188 -10
- package/dist/character/importCharacter.js +854 -76
- package/dist/character/importCharacter.js.map +1 -1
- package/dist/character/importTrust.d.ts +11 -0
- package/dist/character/importTrust.js +56 -0
- package/dist/character/importTrust.js.map +1 -0
- package/dist/character/ktx2Decode.d.ts +11 -0
- package/dist/character/ktx2Decode.js +87 -0
- package/dist/character/ktx2Decode.js.map +1 -0
- package/dist/character/mat4.d.ts +7 -0
- package/dist/character/mat4.js +25 -2
- package/dist/character/mat4.js.map +1 -1
- package/dist/character/meshyToHelixMap.d.ts +14 -0
- package/dist/character/meshyToHelixMap.js +155 -3
- package/dist/character/meshyToHelixMap.js.map +1 -1
- package/dist/character/metahumanToHelixMap.d.ts +2 -0
- package/dist/character/metahumanToHelixMap.js +34 -0
- package/dist/character/metahumanToHelixMap.js.map +1 -0
- package/dist/character/normalMapGuard.d.ts +59 -0
- package/dist/character/normalMapGuard.js +139 -0
- package/dist/character/normalMapGuard.js.map +1 -0
- package/dist/character/quat.js +2 -2
- package/dist/character/quat.js.map +1 -1
- package/dist/character/reframeSkeletonFile.d.ts +19 -0
- package/dist/character/reframeSkeletonFile.js +45 -0
- package/dist/character/reframeSkeletonFile.js.map +1 -0
- package/dist/character/repairLodSkeletonsFile.d.ts +25 -0
- package/dist/character/repairLodSkeletonsFile.js +49 -0
- package/dist/character/repairLodSkeletonsFile.js.map +1 -0
- package/dist/character/restFrameGuard.d.ts +52 -0
- package/dist/character/restFrameGuard.js +90 -0
- package/dist/character/restFrameGuard.js.map +1 -0
- package/dist/character/slotBudgets.d.ts +37 -0
- package/dist/character/slotBudgets.generated.d.ts +15 -0
- package/dist/character/slotBudgets.generated.js +110 -0
- package/dist/character/slotBudgets.generated.js.map +1 -0
- package/dist/character/slotBudgets.js +45 -0
- package/dist/character/slotBudgets.js.map +1 -0
- package/dist/character/stages/atlasMaterials.d.ts +53 -0
- package/dist/character/stages/atlasMaterials.js +246 -0
- package/dist/character/stages/atlasMaterials.js.map +1 -0
- package/dist/character/stages/bakeTexture.d.ts +28 -7
- package/dist/character/stages/bakeTexture.js +586 -108
- package/dist/character/stages/bakeTexture.js.map +1 -1
- package/dist/character/stages/conform.d.ts +54 -9
- package/dist/character/stages/conform.js +686 -113
- package/dist/character/stages/conform.js.map +1 -1
- package/dist/character/stages/earClearance.d.ts +105 -0
- package/dist/character/stages/earClearance.js +480 -0
- package/dist/character/stages/earClearance.js.map +1 -0
- package/dist/character/stages/faceRegion.d.ts +55 -0
- package/dist/character/stages/faceRegion.js +183 -0
- package/dist/character/stages/faceRegion.js.map +1 -0
- package/dist/character/stages/firstPersonHide.d.ts +42 -0
- package/dist/character/stages/firstPersonHide.js +140 -0
- package/dist/character/stages/firstPersonHide.js.map +1 -0
- package/dist/character/stages/joinPerMesh.d.ts +18 -0
- package/dist/character/stages/joinPerMesh.js +46 -0
- package/dist/character/stages/joinPerMesh.js.map +1 -0
- package/dist/character/stages/ktx2.d.ts +6 -4
- package/dist/character/stages/ktx2.js +81 -29
- package/dist/character/stages/ktx2.js.map +1 -1
- package/dist/character/stages/limbBridge.d.ts +114 -0
- package/dist/character/stages/limbBridge.js +1286 -0
- package/dist/character/stages/limbBridge.js.map +1 -0
- package/dist/character/stages/mergeLods.d.ts +19 -0
- package/dist/character/stages/mergeLods.js +58 -2
- package/dist/character/stages/mergeLods.js.map +1 -1
- package/dist/character/stages/poseAwareFit.d.ts +192 -0
- package/dist/character/stages/poseAwareFit.js +417 -0
- package/dist/character/stages/poseAwareFit.js.map +1 -0
- package/dist/character/stages/poseFit/math.d.ts +141 -0
- package/dist/character/stages/poseFit/math.js +514 -0
- package/dist/character/stages/poseFit/math.js.map +1 -0
- package/dist/character/stages/poseFit/measure.d.ts +88 -0
- package/dist/character/stages/poseFit/measure.js +148 -0
- package/dist/character/stages/poseFit/measure.js.map +1 -0
- package/dist/character/stages/poseFit/model.d.ts +46 -0
- package/dist/character/stages/poseFit/model.js +147 -0
- package/dist/character/stages/poseFit/model.js.map +1 -0
- package/dist/character/stages/poseFit/offsetField.d.ts +76 -0
- package/dist/character/stages/poseFit/offsetField.js +249 -0
- package/dist/character/stages/poseFit/offsetField.js.map +1 -0
- package/dist/character/stages/poseFit/posing.d.ts +52 -0
- package/dist/character/stages/poseFit/posing.js +330 -0
- package/dist/character/stages/poseFit/posing.js.map +1 -0
- package/dist/character/stages/poseFit/testRig.d.ts +31 -0
- package/dist/character/stages/poseFit/testRig.js +85 -0
- package/dist/character/stages/poseFit/testRig.js.map +1 -0
- package/dist/character/stages/reframeSkeletonToHost.d.ts +35 -0
- package/dist/character/stages/reframeSkeletonToHost.js +111 -0
- package/dist/character/stages/reframeSkeletonToHost.js.map +1 -0
- package/dist/character/stages/report.js +0 -2
- package/dist/character/stages/report.js.map +1 -1
- package/dist/character/stages/resizeTextures.js +4 -4
- package/dist/character/stages/resizeTextures.js.map +1 -1
- package/dist/character/stages/retarget.d.ts +26 -2
- package/dist/character/stages/retarget.js +52 -4
- package/dist/character/stages/retarget.js.map +1 -1
- package/dist/character/stages/retargetClip.d.ts +6 -2
- package/dist/character/stages/retargetClip.js +109 -17
- package/dist/character/stages/retargetClip.js.map +1 -1
- package/dist/character/stages/sanitizeMaterials.d.ts +39 -0
- package/dist/character/stages/sanitizeMaterials.js +136 -0
- package/dist/character/stages/sanitizeMaterials.js.map +1 -0
- package/dist/character/stages/simplifyMesh.js +6 -3
- package/dist/character/stages/simplifyMesh.js.map +1 -1
- package/dist/character/stages/splitFacePrimitive.js +0 -2
- package/dist/character/stages/splitFacePrimitive.js.map +1 -1
- package/dist/character/stages/splitSharedLodSkeletons.d.ts +31 -0
- package/dist/character/stages/splitSharedLodSkeletons.js +104 -0
- package/dist/character/stages/splitSharedLodSkeletons.js.map +1 -0
- package/dist/character/stages/unifyMaterial.d.ts +66 -0
- package/dist/character/stages/unifyMaterial.js +136 -0
- package/dist/character/stages/unifyMaterial.js.map +1 -0
- package/dist/character/stages/vrmDynamics.d.ts +144 -0
- package/dist/character/stages/vrmDynamics.js +421 -0
- package/dist/character/stages/vrmDynamics.js.map +1 -0
- package/dist/character/stages/vrmFace.d.ts +82 -0
- package/dist/character/stages/vrmFace.js +280 -0
- package/dist/character/stages/vrmFace.js.map +1 -0
- package/dist/character/stages/weldedSimplify.d.ts +15 -1
- package/dist/character/stages/weldedSimplify.js +90 -9
- package/dist/character/stages/weldedSimplify.js.map +1 -1
- package/dist/character/testutil/meshyBridgeFixture.d.ts +33 -0
- package/dist/character/testutil/meshyBridgeFixture.js +219 -0
- package/dist/character/testutil/meshyBridgeFixture.js.map +1 -0
- package/dist/character/testutil/meshyHead.d.ts +19 -0
- package/dist/character/testutil/meshyHead.js +99 -0
- package/dist/character/testutil/meshyHead.js.map +1 -0
- package/dist/character/testutil/meshyRig.d.ts +17 -0
- package/dist/character/testutil/meshyRig.js +117 -0
- package/dist/character/testutil/meshyRig.js.map +1 -0
- package/dist/character/testutil/slotBudgetExpectations.d.ts +11 -0
- package/dist/character/testutil/slotBudgetExpectations.js +21 -0
- package/dist/character/testutil/slotBudgetExpectations.js.map +1 -0
- package/dist/character/testutil/syntheticRig.d.ts +12 -1
- package/dist/character/testutil/syntheticRig.js +88 -3
- package/dist/character/testutil/syntheticRig.js.map +1 -1
- package/dist/character/unrealMaterialSidecar.d.ts +94 -0
- package/dist/character/unrealMaterialSidecar.js +879 -0
- package/dist/character/unrealMaterialSidecar.js.map +1 -0
- package/dist/character/untrustedSource.d.ts +54 -0
- package/dist/character/untrustedSource.js +181 -0
- package/dist/character/untrustedSource.js.map +1 -0
- package/dist/character/vrmCharacter.d.ts +156 -0
- package/dist/character/vrmCharacter.js +678 -0
- package/dist/character/vrmCharacter.js.map +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +72 -24
- package/dist/config.js.map +1 -1
- package/dist/continuum/adminPackagePinCommand.d.ts +14 -0
- package/dist/continuum/adminPackagePinCommand.js +46 -0
- package/dist/continuum/adminPackagePinCommand.js.map +1 -0
- package/dist/continuum/adminPackagePublishCommand.d.ts +13 -0
- package/dist/continuum/adminPackagePublishCommand.js +22 -0
- package/dist/continuum/adminPackagePublishCommand.js.map +1 -0
- package/dist/continuum/authoring.d.ts +52 -0
- package/dist/continuum/authoring.js +143 -0
- package/dist/continuum/authoring.js.map +1 -0
- package/dist/continuum/characterMigrationGenerator.d.ts +17 -0
- package/dist/continuum/characterMigrationGenerator.js +242 -0
- package/dist/continuum/characterMigrationGenerator.js.map +1 -0
- package/dist/continuum/cli.d.ts +69 -0
- package/dist/continuum/cli.js +145 -0
- package/dist/continuum/cli.js.map +1 -0
- package/dist/continuum/cliAuthority.d.ts +17 -0
- package/dist/continuum/cliAuthority.js +28 -0
- package/dist/continuum/cliAuthority.js.map +1 -0
- package/dist/continuum/client.d.ts +103 -0
- package/dist/continuum/client.js +190 -0
- package/dist/continuum/client.js.map +1 -0
- package/dist/continuum/homeScenePackage.d.ts +159 -0
- package/dist/continuum/homeScenePackage.js +267 -0
- package/dist/continuum/homeScenePackage.js.map +1 -0
- package/dist/continuum/homeScenePackageCommand.d.ts +44 -0
- package/dist/continuum/homeScenePackageCommand.js +107 -0
- package/dist/continuum/homeScenePackageCommand.js.map +1 -0
- package/dist/continuum/identity.d.ts +4 -0
- package/dist/continuum/identity.js +20 -0
- package/dist/continuum/identity.js.map +1 -0
- package/dist/continuum/index.d.ts +11 -0
- package/dist/continuum/index.js +28 -0
- package/dist/continuum/index.js.map +1 -0
- package/dist/continuum/missingNormals.d.ts +48 -0
- package/dist/continuum/missingNormals.js +99 -0
- package/dist/continuum/missingNormals.js.map +1 -0
- package/dist/continuum/objectUpload.d.ts +64 -0
- package/dist/continuum/objectUpload.js +375 -0
- package/dist/continuum/objectUpload.js.map +1 -0
- package/dist/continuum/pull.d.ts +22 -0
- package/dist/continuum/pull.js +47 -0
- package/dist/continuum/pull.js.map +1 -0
- package/dist/continuum/sourceBundle.d.ts +18 -0
- package/dist/continuum/sourceBundle.js +160 -0
- package/dist/continuum/sourceBundle.js.map +1 -0
- package/dist/continuum/state.d.ts +41 -0
- package/dist/continuum/state.js +198 -0
- package/dist/continuum/state.js.map +1 -0
- package/dist/continuum/types.d.ts +156 -0
- package/dist/continuum/types.js +6 -0
- package/dist/continuum/types.js.map +1 -0
- package/dist/continuum/workflow.d.ts +62 -0
- package/dist/continuum/workflow.js +592 -0
- package/dist/continuum/workflow.js.map +1 -0
- package/dist/cs2/areas.d.ts +41 -0
- package/dist/cs2/areas.js +82 -0
- package/dist/cs2/areas.js.map +1 -0
- package/dist/cs2/binary.d.ts +10 -0
- package/dist/cs2/binary.js +59 -0
- package/dist/cs2/binary.js.map +1 -0
- package/dist/cs2/cityLegacy.d.ts +29 -0
- package/dist/cs2/cityLegacy.js +48 -0
- package/dist/cs2/cityLegacy.js.map +1 -0
- package/dist/cs2/convert/areas.d.ts +32 -0
- package/dist/cs2/convert/areas.js +96 -0
- package/dist/cs2/convert/areas.js.map +1 -0
- package/dist/cs2/convert/glb.d.ts +48 -0
- package/dist/cs2/convert/glb.js +178 -0
- package/dist/cs2/convert/glb.js.map +1 -0
- package/dist/cs2/convert/markings.d.ts +96 -0
- package/dist/cs2/convert/markings.js +329 -0
- package/dist/cs2/convert/markings.js.map +1 -0
- package/dist/cs2/convert/nodeSeams.d.ts +20 -0
- package/dist/cs2/convert/nodeSeams.js +164 -0
- package/dist/cs2/convert/nodeSeams.js.map +1 -0
- package/dist/cs2/convert/roadBake.d.ts +112 -0
- package/dist/cs2/convert/roadBake.js +290 -0
- package/dist/cs2/convert/roadBake.js.map +1 -0
- package/dist/cs2/convert/roads.d.ts +110 -0
- package/dist/cs2/convert/roads.js +419 -0
- package/dist/cs2/convert/roads.js.map +1 -0
- package/dist/cs2/convert/terrain.d.ts +67 -0
- package/dist/cs2/convert/terrain.js +141 -0
- package/dist/cs2/convert/terrain.js.map +1 -0
- package/dist/cs2/convert/terrainConform.d.ts +58 -0
- package/dist/cs2/convert/terrainConform.js +284 -0
- package/dist/cs2/convert/terrainConform.js.map +1 -0
- package/dist/cs2/convert/treeRoadClearance.d.ts +17 -0
- package/dist/cs2/convert/treeRoadClearance.js +78 -0
- package/dist/cs2/convert/treeRoadClearance.js.map +1 -0
- package/dist/cs2/convert/trees.d.ts +33 -0
- package/dist/cs2/convert/trees.js +68 -0
- package/dist/cs2/convert/trees.js.map +1 -0
- package/dist/cs2/csm2.d.ts +58 -0
- package/dist/cs2/csm2.js +188 -0
- package/dist/cs2/csm2.js.map +1 -0
- package/dist/cs2/fixtures.d.ts +5 -0
- package/dist/cs2/fixtures.js +15 -0
- package/dist/cs2/fixtures.js.map +1 -0
- package/dist/cs2/impostors.d.ts +31 -0
- package/dist/cs2/impostors.js +42 -0
- package/dist/cs2/impostors.js.map +1 -0
- package/dist/cs2/index.d.ts +22 -0
- package/dist/cs2/index.js +191 -0
- package/dist/cs2/index.js.map +1 -0
- package/dist/cs2/inspect.d.ts +81 -0
- package/dist/cs2/inspect.js +181 -0
- package/dist/cs2/inspect.js.map +1 -0
- package/dist/cs2/lods.d.ts +22 -0
- package/dist/cs2/lods.js +136 -0
- package/dist/cs2/lods.js.map +1 -0
- package/dist/cs2/materials.d.ts +43 -0
- package/dist/cs2/materials.js +137 -0
- package/dist/cs2/materials.js.map +1 -0
- package/dist/cs2/placements.d.ts +57 -0
- package/dist/cs2/placements.js +116 -0
- package/dist/cs2/placements.js.map +1 -0
- package/dist/cs2/roadPieces.d.ts +79 -0
- package/dist/cs2/roadPieces.js +263 -0
- package/dist/cs2/roadPieces.js.map +1 -0
- package/dist/cs2/roads.d.ts +92 -0
- package/dist/cs2/roads.js +329 -0
- package/dist/cs2/roads.js.map +1 -0
- package/dist/cs2/terrain.d.ts +37 -0
- package/dist/cs2/terrain.js +91 -0
- package/dist/cs2/terrain.js.map +1 -0
- package/dist/cs2/types.d.ts +444 -0
- package/dist/cs2/types.js +22 -0
- package/dist/cs2/types.js.map +1 -0
- package/dist/cs2/water.d.ts +26 -0
- package/dist/cs2/water.js +58 -0
- package/dist/cs2/water.js.map +1 -0
- package/dist/currency.d.ts +94 -0
- package/dist/currency.js +425 -0
- package/dist/currency.js.map +1 -0
- package/dist/dev/devServer.d.ts +53 -0
- package/dist/dev/devServer.js +228 -0
- package/dist/dev/devServer.js.map +1 -0
- package/dist/dev/shellPage.d.ts +3 -0
- package/dist/dev/shellPage.js +297 -0
- package/dist/dev/shellPage.js.map +1 -0
- package/dist/dev/state.d.ts +84 -0
- package/dist/dev/state.js +265 -0
- package/dist/dev/state.js.map +1 -0
- package/dist/device.d.ts +104 -0
- package/dist/device.js +2744 -0
- package/dist/device.js.map +1 -0
- package/dist/deviceActionValidation.d.ts +21 -0
- package/dist/deviceActionValidation.js +460 -0
- package/dist/deviceActionValidation.js.map +1 -0
- package/dist/distribution.d.ts +69 -0
- package/dist/distribution.js +138 -0
- package/dist/distribution.js.map +1 -0
- package/dist/enginePins.d.ts +45 -0
- package/dist/enginePins.js +134 -0
- package/dist/enginePins.js.map +1 -0
- package/dist/gesture/axisReference.d.ts +22 -0
- package/dist/gesture/axisReference.js +32 -0
- package/dist/gesture/axisReference.js.map +1 -0
- package/dist/gesture/capture.d.ts +84 -0
- package/dist/gesture/capture.js +303 -0
- package/dist/gesture/capture.js.map +1 -0
- package/dist/gesture/harnessPage.d.ts +23 -0
- package/dist/gesture/harnessPage.js +153 -0
- package/dist/gesture/harnessPage.js.map +1 -0
- package/dist/gesture/inspect.d.ts +189 -0
- package/dist/gesture/inspect.js +678 -0
- package/dist/gesture/inspect.js.map +1 -0
- package/dist/gesture/rig.d.ts +12 -0
- package/dist/gesture/rig.js +111 -0
- package/dist/gesture/rig.js.map +1 -0
- package/dist/gesture/worldRuntime.d.ts +65 -0
- package/dist/gesture/worldRuntime.js +110 -0
- package/dist/gesture/worldRuntime.js.map +1 -0
- package/dist/glb/draw-calls.d.ts +60 -0
- package/dist/glb/draw-calls.js +160 -0
- package/dist/glb/draw-calls.js.map +1 -0
- package/dist/glb/glb-summary.d.ts +68 -0
- package/dist/glb/glb-summary.js +450 -0
- package/dist/glb/glb-summary.js.map +1 -0
- package/dist/glb/index.d.ts +12 -0
- package/dist/glb/index.js +24 -0
- package/dist/glb/index.js.map +1 -0
- package/dist/glb/smooth-normals.d.ts +98 -0
- package/dist/glb/smooth-normals.js +395 -0
- package/dist/glb/smooth-normals.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6786 -128
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +35 -1
- package/dist/init.js +1115 -47
- package/dist/init.js.map +1 -1
- package/dist/install.d.ts +16 -5
- package/dist/install.js +123 -23
- package/dist/install.js.map +1 -1
- package/dist/interactable.d.ts +154 -0
- package/dist/interactable.js +1219 -0
- package/dist/interactable.js.map +1 -0
- package/dist/item.d.ts +217 -0
- package/dist/item.js +892 -0
- package/dist/item.js.map +1 -0
- package/dist/itemDef/distribute.d.ts +17 -0
- package/dist/itemDef/distribute.js +29 -0
- package/dist/itemDef/distribute.js.map +1 -0
- package/dist/itemDef/emoteRules.d.ts +85 -0
- package/dist/itemDef/emoteRules.js +368 -0
- package/dist/itemDef/emoteRules.js.map +1 -0
- package/dist/itemDef/gunConfigRules.d.ts +24 -0
- package/dist/itemDef/gunConfigRules.js +300 -0
- package/dist/itemDef/gunConfigRules.js.map +1 -0
- package/dist/itemDef/inspect.d.ts +19 -0
- package/dist/itemDef/inspect.js +144 -0
- package/dist/itemDef/inspect.js.map +1 -0
- package/dist/itemDef/scaffold.d.ts +41 -0
- package/dist/itemDef/scaffold.js +303 -0
- package/dist/itemDef/scaffold.js.map +1 -0
- package/dist/itemDef/tables.d.ts +81 -0
- package/dist/itemDef/tables.js +226 -0
- package/dist/itemDef/tables.js.map +1 -0
- package/dist/itemDef/validate.d.ts +30 -0
- package/dist/itemDef/validate.js +175 -0
- package/dist/itemDef/validate.js.map +1 -0
- package/dist/itemPublishVersion.d.ts +46 -0
- package/dist/itemPublishVersion.js +242 -0
- package/dist/itemPublishVersion.js.map +1 -0
- package/dist/itemSearch.d.ts +128 -0
- package/dist/itemSearch.js +140 -0
- package/dist/itemSearch.js.map +1 -0
- package/dist/itemSetVersion.d.ts +77 -0
- package/dist/itemSetVersion.js +170 -0
- package/dist/itemSetVersion.js.map +1 -0
- package/dist/itemThumbnail.d.ts +40 -0
- package/dist/itemThumbnail.js +116 -0
- package/dist/itemThumbnail.js.map +1 -0
- package/dist/lib.d.ts +571 -7
- package/dist/lib.js +1178 -15
- package/dist/lib.js.map +1 -1
- package/dist/looks.d.ts +103 -0
- package/dist/looks.js +102 -0
- package/dist/looks.js.map +1 -0
- package/dist/materials/availability.d.ts +8 -0
- package/dist/materials/availability.js +22 -0
- package/dist/materials/availability.js.map +1 -0
- package/dist/materials/identity.d.ts +30 -0
- package/dist/materials/identity.js +68 -0
- package/dist/materials/identity.js.map +1 -0
- package/dist/materials/pins.d.ts +23 -0
- package/dist/materials/pins.js +93 -0
- package/dist/materials/pins.js.map +1 -0
- package/dist/mesh/surfaceBvh.d.ts +13 -0
- package/dist/mesh/surfaceBvh.js +197 -0
- package/dist/mesh/surfaceBvh.js.map +1 -0
- package/dist/module-validation.d.ts +2 -0
- package/dist/module-validation.js +24 -0
- package/dist/module-validation.js.map +1 -0
- package/dist/money.d.ts +183 -0
- package/dist/money.js +299 -0
- package/dist/money.js.map +1 -0
- package/dist/npc/capture.d.ts +41 -0
- package/dist/npc/capture.js +99 -0
- package/dist/npc/capture.js.map +1 -0
- package/dist/npc/format.d.ts +2 -0
- package/dist/npc/format.js +102 -0
- package/dist/npc/format.js.map +1 -0
- package/dist/npc/inspect.d.ts +23 -0
- package/dist/npc/inspect.js +54 -0
- package/dist/npc/inspect.js.map +1 -0
- package/dist/npc/metrics.d.ts +132 -0
- package/dist/npc/metrics.js +214 -0
- package/dist/npc/metrics.js.map +1 -0
- package/dist/npc/session.d.ts +35 -0
- package/dist/npc/session.js +172 -0
- package/dist/npc/session.js.map +1 -0
- package/dist/pack/locomotionReference.d.ts +40 -0
- package/dist/pack/locomotionReference.js +132 -0
- package/dist/pack/locomotionReference.js.map +1 -0
- package/dist/pack/scaffold.d.ts +15 -0
- package/dist/pack/scaffold.js +144 -0
- package/dist/pack/scaffold.js.map +1 -0
- package/dist/pack/validate.d.ts +67 -0
- package/dist/pack/validate.js +317 -0
- package/dist/pack/validate.js.map +1 -0
- package/dist/package.d.ts +14 -1
- package/dist/package.js +106 -7
- package/dist/package.js.map +1 -1
- package/dist/paletteShowcase.d.ts +183 -0
- package/dist/paletteShowcase.js +454 -0
- package/dist/paletteShowcase.js.map +1 -0
- package/dist/paletteShowcaseProfiles.d.ts +112 -0
- package/dist/paletteShowcaseProfiles.js +121 -0
- package/dist/paletteShowcaseProfiles.js.map +1 -0
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +11 -0
- package/dist/paths.js.map +1 -0
- package/dist/placementRequest.d.ts +37 -0
- package/dist/placementRequest.js +220 -0
- package/dist/placementRequest.js.map +1 -0
- package/dist/placements/assets.d.ts +7 -0
- package/dist/placements/assets.js +114 -0
- package/dist/placements/assets.js.map +1 -0
- package/dist/placements/attachFoliageLods.d.ts +4 -0
- package/dist/placements/attachFoliageLods.js +70 -0
- package/dist/placements/attachFoliageLods.js.map +1 -0
- package/dist/placements/attachImpostors.d.ts +3 -0
- package/dist/placements/attachImpostors.js +51 -0
- package/dist/placements/attachImpostors.js.map +1 -0
- package/dist/placements/authored.d.ts +11 -0
- package/dist/placements/authored.js +65 -0
- package/dist/placements/authored.js.map +1 -0
- package/dist/placements/bake.d.ts +2 -0
- package/dist/placements/bake.js +235 -0
- package/dist/placements/bake.js.map +1 -0
- package/dist/placements/captureHost.d.ts +4 -0
- package/dist/placements/captureHost.js +125 -0
- package/dist/placements/captureHost.js.map +1 -0
- package/dist/placements/foliageCapture.d.ts +2 -0
- package/dist/placements/foliageCapture.js +98 -0
- package/dist/placements/foliageCapture.js.map +1 -0
- package/dist/placements/foliageCards.d.ts +17 -0
- package/dist/placements/foliageCards.js +120 -0
- package/dist/placements/foliageCards.js.map +1 -0
- package/dist/placements/foliageLods.d.ts +8 -0
- package/dist/placements/foliageLods.js +240 -0
- package/dist/placements/foliageLods.js.map +1 -0
- package/dist/placements/foliageTexture.d.ts +4 -0
- package/dist/placements/foliageTexture.js +91 -0
- package/dist/placements/foliageTexture.js.map +1 -0
- package/dist/placements/impostorCapture.d.ts +2 -0
- package/dist/placements/impostorCapture.js +111 -0
- package/dist/placements/impostorCapture.js.map +1 -0
- package/dist/placements/impostors.d.ts +18 -0
- package/dist/placements/impostors.js +279 -0
- package/dist/placements/impostors.js.map +1 -0
- package/dist/placements/source.d.ts +53 -0
- package/dist/placements/source.js +111 -0
- package/dist/placements/source.js.map +1 -0
- package/dist/product.d.ts +84 -0
- package/dist/product.js +285 -0
- package/dist/product.js.map +1 -0
- package/dist/publications.d.ts +116 -0
- package/dist/publications.js +180 -0
- package/dist/publications.js.map +1 -0
- package/dist/publishDuplicateGuard.d.ts +92 -0
- package/dist/publishDuplicateGuard.js +128 -0
- package/dist/publishDuplicateGuard.js.map +1 -0
- package/dist/realizability.d.ts +36 -0
- package/dist/realizability.js +269 -0
- package/dist/realizability.js.map +1 -0
- package/dist/resolve.d.ts +7 -1
- package/dist/resolve.js +77 -11
- package/dist/resolve.js.map +1 -1
- package/dist/sceneElements/compiler.d.ts +21 -0
- package/dist/sceneElements/compiler.js +976 -0
- package/dist/sceneElements/compiler.js.map +1 -0
- package/dist/sceneElements/continuum.d.ts +54 -0
- package/dist/sceneElements/continuum.js +451 -0
- package/dist/sceneElements/continuum.js.map +1 -0
- package/dist/sceneElements/devices.d.ts +12 -0
- package/dist/sceneElements/devices.js +212 -0
- package/dist/sceneElements/devices.js.map +1 -0
- package/dist/sceneElements/meshCells.d.ts +49 -0
- package/dist/sceneElements/meshCells.js +174 -0
- package/dist/sceneElements/meshCells.js.map +1 -0
- package/dist/sceneElements/types.d.ts +261 -0
- package/dist/sceneElements/types.js +3 -0
- package/dist/sceneElements/types.js.map +1 -0
- package/dist/sceneHlod/approximate.d.ts +23 -0
- package/dist/sceneHlod/approximate.js +171 -0
- package/dist/sceneHlod/approximate.js.map +1 -0
- package/dist/sceneHlod/artifact.d.ts +3 -0
- package/dist/sceneHlod/artifact.js +29 -0
- package/dist/sceneHlod/artifact.js.map +1 -0
- package/dist/sceneHlod/bakeSurface.d.ts +2 -0
- package/dist/sceneHlod/bakeSurface.js +59 -0
- package/dist/sceneHlod/bakeSurface.js.map +1 -0
- package/dist/sceneHlod/basisEncoder.d.ts +3 -0
- package/dist/sceneHlod/basisEncoder.js +68 -0
- package/dist/sceneHlod/basisEncoder.js.map +1 -0
- package/dist/sceneHlod/cellArtifact.d.ts +17 -0
- package/dist/sceneHlod/cellArtifact.js +78 -0
- package/dist/sceneHlod/cellArtifact.js.map +1 -0
- package/dist/sceneHlod/commands.d.ts +2 -0
- package/dist/sceneHlod/commands.js +114 -0
- package/dist/sceneHlod/commands.js.map +1 -0
- package/dist/sceneHlod/complete.d.ts +78 -0
- package/dist/sceneHlod/complete.js +261 -0
- package/dist/sceneHlod/complete.js.map +1 -0
- package/dist/sceneHlod/completePrototype.d.ts +39 -0
- package/dist/sceneHlod/completePrototype.js +121 -0
- package/dist/sceneHlod/completePrototype.js.map +1 -0
- package/dist/sceneHlod/geometry.d.ts +6 -0
- package/dist/sceneHlod/geometry.js +101 -0
- package/dist/sceneHlod/geometry.js.map +1 -0
- package/dist/sceneHlod/imageSampling.d.ts +11 -0
- package/dist/sceneHlod/imageSampling.js +100 -0
- package/dist/sceneHlod/imageSampling.js.map +1 -0
- package/dist/sceneHlod/refinement.d.ts +46 -0
- package/dist/sceneHlod/refinement.js +204 -0
- package/dist/sceneHlod/refinement.js.map +1 -0
- package/dist/sceneHlod/scene.d.ts +80 -0
- package/dist/sceneHlod/scene.js +264 -0
- package/dist/sceneHlod/scene.js.map +1 -0
- package/dist/sceneHlod/sharedAtlas.d.ts +37 -0
- package/dist/sceneHlod/sharedAtlas.js +84 -0
- package/dist/sceneHlod/sharedAtlas.js.map +1 -0
- package/dist/sceneHlod/source.d.ts +17 -0
- package/dist/sceneHlod/source.js +241 -0
- package/dist/sceneHlod/source.js.map +1 -0
- package/dist/sceneHlod/textureCodec.d.ts +4 -0
- package/dist/sceneHlod/textureCodec.js +66 -0
- package/dist/sceneHlod/textureCodec.js.map +1 -0
- package/dist/sceneHlod/transfer.d.ts +8 -0
- package/dist/sceneHlod/transfer.js +147 -0
- package/dist/sceneHlod/transfer.js.map +1 -0
- package/dist/sceneHlod/types.d.ts +69 -0
- package/dist/sceneHlod/types.js +3 -0
- package/dist/sceneHlod/types.js.map +1 -0
- package/dist/sceneImport/archive.d.ts +30 -0
- package/dist/sceneImport/archive.js +336 -0
- package/dist/sceneImport/archive.js.map +1 -0
- package/dist/sceneImport/bistroMaterialBake.d.ts +20 -0
- package/dist/sceneImport/bistroMaterialBake.js +169 -0
- package/dist/sceneImport/bistroMaterialBake.js.map +1 -0
- package/dist/sceneImport/bistroTexturePlan.d.ts +6 -0
- package/dist/sceneImport/bistroTexturePlan.js +23 -0
- package/dist/sceneImport/bistroTexturePlan.js.map +1 -0
- package/dist/sceneImport/compiler.d.ts +36 -0
- package/dist/sceneImport/compiler.js +251 -0
- package/dist/sceneImport/compiler.js.map +1 -0
- package/dist/sceneImport/composeMounts.d.ts +159 -0
- package/dist/sceneImport/composeMounts.js +687 -0
- package/dist/sceneImport/composeMounts.js.map +1 -0
- package/dist/sceneImport/defaultMaterials.d.ts +45 -0
- package/dist/sceneImport/defaultMaterials.js +121 -0
- package/dist/sceneImport/defaultMaterials.js.map +1 -0
- package/dist/sceneImport/externalTextures.d.ts +44 -0
- package/dist/sceneImport/externalTextures.js +146 -0
- package/dist/sceneImport/externalTextures.js.map +1 -0
- package/dist/sceneImport/fbx.d.ts +101 -0
- package/dist/sceneImport/fbx.js +307 -0
- package/dist/sceneImport/fbx.js.map +1 -0
- package/dist/sceneImport/glbSplit.d.ts +24 -0
- package/dist/sceneImport/glbSplit.js +95 -0
- package/dist/sceneImport/glbSplit.js.map +1 -0
- package/dist/sceneImport/gltfIo.d.ts +32 -0
- package/dist/sceneImport/gltfIo.js +39 -0
- package/dist/sceneImport/gltfIo.js.map +1 -0
- package/dist/sceneImport/helsinkiTiles.d.ts +31 -0
- package/dist/sceneImport/helsinkiTiles.js +52 -0
- package/dist/sceneImport/helsinkiTiles.js.map +1 -0
- package/dist/sceneImport/materials/binding.d.ts +16 -0
- package/dist/sceneImport/materials/binding.js +42 -0
- package/dist/sceneImport/materials/binding.js.map +1 -0
- package/dist/sceneImport/materials/catalog.d.ts +36 -0
- package/dist/sceneImport/materials/catalog.js +233 -0
- package/dist/sceneImport/materials/catalog.js.map +1 -0
- package/dist/sceneImport/materials/index.d.ts +4 -0
- package/dist/sceneImport/materials/index.js +21 -0
- package/dist/sceneImport/materials/index.js.map +1 -0
- package/dist/sceneImport/materials/matcher.d.ts +33 -0
- package/dist/sceneImport/materials/matcher.js +127 -0
- package/dist/sceneImport/materials/matcher.js.map +1 -0
- package/dist/sceneImport/materials/recipe.d.ts +30 -0
- package/dist/sceneImport/materials/recipe.js +134 -0
- package/dist/sceneImport/materials/recipe.js.map +1 -0
- package/dist/sceneImport/multiAsset.d.ts +79 -0
- package/dist/sceneImport/multiAsset.js +535 -0
- package/dist/sceneImport/multiAsset.js.map +1 -0
- package/dist/sceneImport/multiAssetSpawn.d.ts +40 -0
- package/dist/sceneImport/multiAssetSpawn.js +98 -0
- package/dist/sceneImport/multiAssetSpawn.js.map +1 -0
- package/dist/sceneImport/obj.d.ts +41 -0
- package/dist/sceneImport/obj.js +276 -0
- package/dist/sceneImport/obj.js.map +1 -0
- package/dist/sceneImport/package.d.ts +104 -0
- package/dist/sceneImport/package.js +1004 -0
- package/dist/sceneImport/package.js.map +1 -0
- package/dist/sceneImport/packageReader.d.ts +10 -0
- package/dist/sceneImport/packageReader.js +250 -0
- package/dist/sceneImport/packageReader.js.map +1 -0
- package/dist/sceneImport/proxy.d.ts +100 -0
- package/dist/sceneImport/proxy.js +334 -0
- package/dist/sceneImport/proxy.js.map +1 -0
- package/dist/sceneImport/publish.d.ts +47 -0
- package/dist/sceneImport/publish.js +98 -0
- package/dist/sceneImport/publish.js.map +1 -0
- package/dist/sceneImport/semantic.d.ts +75 -0
- package/dist/sceneImport/semantic.js +296 -0
- package/dist/sceneImport/semantic.js.map +1 -0
- package/dist/sceneImport/spawnProbe.d.ts +55 -0
- package/dist/sceneImport/spawnProbe.js +159 -0
- package/dist/sceneImport/spawnProbe.js.map +1 -0
- package/dist/sceneImport/streamLayout.d.ts +96 -0
- package/dist/sceneImport/streamLayout.js +203 -0
- package/dist/sceneImport/streamLayout.js.map +1 -0
- package/dist/sceneImport/textureDecode.d.ts +21 -0
- package/dist/sceneImport/textureDecode.js +292 -0
- package/dist/sceneImport/textureDecode.js.map +1 -0
- package/dist/sceneImport/types.d.ts +84 -0
- package/dist/sceneImport/types.js +58 -0
- package/dist/sceneImport/types.js.map +1 -0
- package/dist/scenePerformance.d.ts +162 -0
- package/dist/scenePerformance.js +905 -0
- package/dist/scenePerformance.js.map +1 -0
- package/dist/screenshot/captureScreenshots.d.ts +36 -0
- package/dist/screenshot/captureScreenshots.js +229 -0
- package/dist/screenshot/captureScreenshots.js.map +1 -0
- package/dist/screenshot/encodeImagePreview.d.ts +5 -0
- package/dist/screenshot/encodeImagePreview.js +42 -0
- package/dist/screenshot/encodeImagePreview.js.map +1 -0
- package/dist/screenshot/publishThumbnail.d.ts +4 -0
- package/dist/screenshot/publishThumbnail.js +56 -0
- package/dist/screenshot/publishThumbnail.js.map +1 -0
- package/dist/screenshot/staticServe.d.ts +14 -0
- package/dist/screenshot/staticServe.js +120 -0
- package/dist/screenshot/staticServe.js.map +1 -0
- package/dist/source-archive.d.ts +101 -0
- package/dist/source-archive.js +309 -0
- package/dist/source-archive.js.map +1 -0
- package/dist/stdioBlocking.d.ts +1 -0
- package/dist/stdioBlocking.js +9 -0
- package/dist/stdioBlocking.js.map +1 -0
- package/dist/terrain/bake.d.ts +40 -0
- package/dist/terrain/bake.js +318 -0
- package/dist/terrain/bake.js.map +1 -0
- package/dist/terrain/generate.d.ts +21 -0
- package/dist/terrain/generate.js +239 -0
- package/dist/terrain/generate.js.map +1 -0
- package/dist/terrain/grid.d.ts +48 -0
- package/dist/terrain/grid.js +133 -0
- package/dist/terrain/grid.js.map +1 -0
- package/dist/terrain/import.d.ts +40 -0
- package/dist/terrain/import.js +252 -0
- package/dist/terrain/import.js.map +1 -0
- package/dist/terrain/index.d.ts +11 -0
- package/dist/terrain/index.js +88 -0
- package/dist/terrain/index.js.map +1 -0
- package/dist/terrain/inspect.d.ts +35 -0
- package/dist/terrain/inspect.js +115 -0
- package/dist/terrain/inspect.js.map +1 -0
- package/dist/terrain/island.d.ts +31 -0
- package/dist/terrain/island.js +128 -0
- package/dist/terrain/island.js.map +1 -0
- package/dist/terrain/noise.d.ts +79 -0
- package/dist/terrain/noise.js +162 -0
- package/dist/terrain/noise.js.map +1 -0
- package/dist/terrain/normals.d.ts +21 -0
- package/dist/terrain/normals.js +55 -0
- package/dist/terrain/normals.js.map +1 -0
- package/dist/terrain/presets/alpine.json +20 -0
- package/dist/terrain/presets/coast.json +20 -0
- package/dist/terrain/presets/desert.json +22 -0
- package/dist/terrain/presets/forest.json +20 -0
- package/dist/terrain/presets/meadow.json +20 -0
- package/dist/terrain/presets/volcanic.json +22 -0
- package/dist/terrain/presets.d.ts +29 -0
- package/dist/terrain/presets.js +128 -0
- package/dist/terrain/presets.js.map +1 -0
- package/dist/terrain/source.d.ts +42 -0
- package/dist/terrain/source.js +241 -0
- package/dist/terrain/source.js.map +1 -0
- package/dist/terrain/tile.d.ts +92 -0
- package/dist/terrain/tile.js +268 -0
- package/dist/terrain/tile.js.map +1 -0
- package/dist/textureAudit.d.ts +23 -0
- package/dist/textureAudit.js +192 -0
- package/dist/textureAudit.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/unreal/avatarMode.d.ts +21 -0
- package/dist/unreal/avatarMode.js +33 -0
- package/dist/unreal/avatarMode.js.map +1 -0
- package/dist/unreal/delivery.d.ts +46 -0
- package/dist/unreal/delivery.js +89 -0
- package/dist/unreal/delivery.js.map +1 -0
- package/dist/unreal/doctor.d.ts +50 -0
- package/dist/unreal/doctor.js +102 -0
- package/dist/unreal/doctor.js.map +1 -0
- package/dist/unreal/package.d.ts +80 -0
- package/dist/unreal/package.js +273 -0
- package/dist/unreal/package.js.map +1 -0
- package/dist/unreal/release.d.ts +41 -0
- package/dist/unreal/release.js +122 -0
- package/dist/unreal/release.js.map +1 -0
- package/dist/unreal.d.ts +30 -0
- package/dist/unreal.js +362 -0
- package/dist/unreal.js.map +1 -0
- package/dist/unrealCandidate.d.ts +213 -0
- package/dist/unrealCandidate.js +765 -0
- package/dist/unrealCandidate.js.map +1 -0
- package/dist/unrealManifest.d.ts +74 -0
- package/dist/unrealManifest.js +354 -0
- package/dist/unrealManifest.js.map +1 -0
- package/dist/unrealPublish.d.ts +111 -0
- package/dist/unrealPublish.js +331 -0
- package/dist/unrealPublish.js.map +1 -0
- package/dist/vault.d.ts +121 -0
- package/dist/vault.js +136 -0
- package/dist/vault.js.map +1 -0
- package/dist/vehicle/addOnDefinition.d.ts +19 -0
- package/dist/vehicle/addOnDefinition.js +21 -0
- package/dist/vehicle/addOnDefinition.js.map +1 -0
- package/dist/vehicle/addon.d.ts +158 -0
- package/dist/vehicle/addon.js +441 -0
- package/dist/vehicle/addon.js.map +1 -0
- package/dist/vehicle/addonDefinitionGuard.d.ts +53 -0
- package/dist/vehicle/addonDefinitionGuard.js +206 -0
- package/dist/vehicle/addonDefinitionGuard.js.map +1 -0
- package/dist/vehicle/addonPublished.d.ts +80 -0
- package/dist/vehicle/addonPublished.js +221 -0
- package/dist/vehicle/addonPublished.js.map +1 -0
- package/dist/vehicle/audio.d.ts +259 -0
- package/dist/vehicle/audio.js +1069 -0
- package/dist/vehicle/audio.js.map +1 -0
- package/dist/vehicle/audioDefects.d.ts +125 -0
- package/dist/vehicle/audioDefects.js +591 -0
- package/dist/vehicle/audioDefects.js.map +1 -0
- package/dist/vehicle/audioDistinct.d.ts +62 -0
- package/dist/vehicle/audioDistinct.js +479 -0
- package/dist/vehicle/audioDistinct.js.map +1 -0
- package/dist/vehicle/audioImport.d.ts +217 -0
- package/dist/vehicle/audioImport.js +610 -0
- package/dist/vehicle/audioImport.js.map +1 -0
- package/dist/vehicle/audioProvenance.d.ts +47 -0
- package/dist/vehicle/audioProvenance.js +364 -0
- package/dist/vehicle/audioProvenance.js.map +1 -0
- package/dist/vehicle/audioReference.d.ts +36 -0
- package/dist/vehicle/audioReference.js +176 -0
- package/dist/vehicle/audioReference.js.map +1 -0
- package/dist/vehicle/audioSlots.d.ts +27 -0
- package/dist/vehicle/audioSlots.js +43 -0
- package/dist/vehicle/audioSlots.js.map +1 -0
- package/dist/vehicle/audioSpectral.d.ts +71 -0
- package/dist/vehicle/audioSpectral.js +534 -0
- package/dist/vehicle/audioSpectral.js.map +1 -0
- package/dist/vehicle/cabinCheck.d.ts +209 -0
- package/dist/vehicle/cabinCheck.js +799 -0
- package/dist/vehicle/cabinCheck.js.map +1 -0
- package/dist/vehicle/drive.d.ts +66 -0
- package/dist/vehicle/drive.js +228 -0
- package/dist/vehicle/drive.js.map +1 -0
- package/dist/vehicle/enginePolicy.d.ts +68 -0
- package/dist/vehicle/enginePolicy.js +209 -0
- package/dist/vehicle/enginePolicy.js.map +1 -0
- package/dist/vehicle/hostFrame.d.ts +78 -0
- package/dist/vehicle/hostFrame.js +136 -0
- package/dist/vehicle/hostFrame.js.map +1 -0
- package/dist/vehicle/inspectionSheets.d.ts +42 -0
- package/dist/vehicle/inspectionSheets.js +173 -0
- package/dist/vehicle/inspectionSheets.js.map +1 -0
- package/dist/vehicle/keyboardLaunch.d.ts +189 -0
- package/dist/vehicle/keyboardLaunch.js +340 -0
- package/dist/vehicle/keyboardLaunch.js.map +1 -0
- package/dist/vehicle/lampCoverage.d.ts +49 -0
- package/dist/vehicle/lampCoverage.js +252 -0
- package/dist/vehicle/lampCoverage.js.map +1 -0
- package/dist/vehicle/mintAddOn.d.ts +110 -0
- package/dist/vehicle/mintAddOn.js +321 -0
- package/dist/vehicle/mintAddOn.js.map +1 -0
- package/dist/vehicle/mintVehicle.d.ts +189 -0
- package/dist/vehicle/mintVehicle.js +417 -0
- package/dist/vehicle/mintVehicle.js.map +1 -0
- package/dist/vehicle/paintHue.d.ts +89 -0
- package/dist/vehicle/paintHue.js +248 -0
- package/dist/vehicle/paintHue.js.map +1 -0
- package/dist/vehicle/panelCoverage.d.ts +37 -0
- package/dist/vehicle/panelCoverage.js +93 -0
- package/dist/vehicle/panelCoverage.js.map +1 -0
- package/dist/vehicle/platform.d.ts +71 -0
- package/dist/vehicle/platform.js +144 -0
- package/dist/vehicle/platform.js.map +1 -0
- package/dist/vehicle/preview.d.ts +339 -0
- package/dist/vehicle/preview.js +854 -0
- package/dist/vehicle/preview.js.map +1 -0
- package/dist/vehicle/previewAddon.d.ts +197 -0
- package/dist/vehicle/previewAddon.js +520 -0
- package/dist/vehicle/previewAddon.js.map +1 -0
- package/dist/vehicle/previewSeats.d.ts +102 -0
- package/dist/vehicle/previewSeats.js +190 -0
- package/dist/vehicle/previewSeats.js.map +1 -0
- package/dist/vehicle/publish.d.ts +78 -0
- package/dist/vehicle/publish.js +111 -0
- package/dist/vehicle/publish.js.map +1 -0
- package/dist/vehicle/recheckListed.d.ts +87 -0
- package/dist/vehicle/recheckListed.js +200 -0
- package/dist/vehicle/recheckListed.js.map +1 -0
- package/dist/vehicle/renditionManifest.d.ts +10 -0
- package/dist/vehicle/renditionManifest.js +56 -0
- package/dist/vehicle/renditionManifest.js.map +1 -0
- package/dist/vehicle/sealCheck.d.ts +30 -0
- package/dist/vehicle/sealCheck.js +59 -0
- package/dist/vehicle/sealCheck.js.map +1 -0
- package/dist/vehicle/simulate.d.ts +219 -0
- package/dist/vehicle/simulate.js +536 -0
- package/dist/vehicle/simulate.js.map +1 -0
- package/dist/vehicle/simulateRuntime.d.ts +76 -0
- package/dist/vehicle/simulateRuntime.js +252 -0
- package/dist/vehicle/simulateRuntime.js.map +1 -0
- package/dist/vehicle/testutil/sourcedAudio.d.ts +13 -0
- package/dist/vehicle/testutil/sourcedAudio.js +96 -0
- package/dist/vehicle/testutil/sourcedAudio.js.map +1 -0
- package/dist/vehicle/validate.d.ts +225 -0
- package/dist/vehicle/validate.js +956 -0
- package/dist/vehicle/validate.js.map +1 -0
- package/dist/vehicle/wheelDesign.d.ts +98 -0
- package/dist/vehicle/wheelDesign.js +276 -0
- package/dist/vehicle/wheelDesign.js.map +1 -0
- package/dist/verifySubPath.d.ts +69 -0
- package/dist/verifySubPath.js +458 -0
- package/dist/verifySubPath.js.map +1 -0
- package/dist/visualQa/animation.d.ts +57 -0
- package/dist/visualQa/animation.js +503 -0
- package/dist/visualQa/animation.js.map +1 -0
- package/dist/visualQa/avatar.d.ts +3 -0
- package/dist/visualQa/avatar.js +443 -0
- package/dist/visualQa/avatar.js.map +1 -0
- package/dist/visualQa/avatarMath.d.ts +180 -0
- package/dist/visualQa/avatarMath.js +318 -0
- package/dist/visualQa/avatarMath.js.map +1 -0
- package/dist/visualQa/avatarPreview.d.ts +26 -0
- package/dist/visualQa/avatarPreview.js +167 -0
- package/dist/visualQa/avatarPreview.js.map +1 -0
- package/dist/visualQa/avatarPreviewMath.d.ts +189 -0
- package/dist/visualQa/avatarPreviewMath.js +696 -0
- package/dist/visualQa/avatarPreviewMath.js.map +1 -0
- package/dist/visualQa/avatarPreviewProbe.d.ts +7 -0
- package/dist/visualQa/avatarPreviewProbe.js +181 -0
- package/dist/visualQa/avatarPreviewProbe.js.map +1 -0
- package/dist/visualQa/avatarProbe.d.ts +1 -0
- package/dist/visualQa/avatarProbe.js +264 -0
- package/dist/visualQa/avatarProbe.js.map +1 -0
- package/dist/visualQa/browser.d.ts +116 -0
- package/dist/visualQa/browser.js +817 -0
- package/dist/visualQa/browser.js.map +1 -0
- package/dist/visualQa/candidate.d.ts +116 -0
- package/dist/visualQa/candidate.js +265 -0
- package/dist/visualQa/candidate.js.map +1 -0
- package/dist/visualQa/checklists.d.ts +31 -0
- package/dist/visualQa/checklists.js +160 -0
- package/dist/visualQa/checklists.js.map +1 -0
- package/dist/visualQa/command.d.ts +110 -0
- package/dist/visualQa/command.js +521 -0
- package/dist/visualQa/command.js.map +1 -0
- package/dist/visualQa/commonAsserts.d.ts +68 -0
- package/dist/visualQa/commonAsserts.js +239 -0
- package/dist/visualQa/commonAsserts.js.map +1 -0
- package/dist/visualQa/contactSheet.d.ts +32 -0
- package/dist/visualQa/contactSheet.js +153 -0
- package/dist/visualQa/contactSheet.js.map +1 -0
- package/dist/visualQa/critic.d.ts +18 -0
- package/dist/visualQa/critic.js +179 -0
- package/dist/visualQa/critic.js.map +1 -0
- package/dist/visualQa/emotePage.d.ts +10 -0
- package/dist/visualQa/emotePage.js +249 -0
- package/dist/visualQa/emotePage.js.map +1 -0
- package/dist/visualQa/evidenceOverlay.d.ts +74 -0
- package/dist/visualQa/evidenceOverlay.js +268 -0
- package/dist/visualQa/evidenceOverlay.js.map +1 -0
- package/dist/visualQa/harness.d.ts +57 -0
- package/dist/visualQa/harness.js +3 -0
- package/dist/visualQa/harness.js.map +1 -0
- package/dist/visualQa/hosted.d.ts +152 -0
- package/dist/visualQa/hosted.js +367 -0
- package/dist/visualQa/hosted.js.map +1 -0
- package/dist/visualQa/labelLimit.d.ts +12 -0
- package/dist/visualQa/labelLimit.js +31 -0
- package/dist/visualQa/labelLimit.js.map +1 -0
- package/dist/visualQa/mirror.d.ts +36 -0
- package/dist/visualQa/mirror.js +98 -0
- package/dist/visualQa/mirror.js.map +1 -0
- package/dist/visualQa/moveUx.d.ts +44 -0
- package/dist/visualQa/moveUx.js +103 -0
- package/dist/visualQa/moveUx.js.map +1 -0
- package/dist/visualQa/pinnedChrome.d.ts +63 -0
- package/dist/visualQa/pinnedChrome.js +214 -0
- package/dist/visualQa/pinnedChrome.js.map +1 -0
- package/dist/visualQa/resolve.d.ts +88 -0
- package/dist/visualQa/resolve.js +248 -0
- package/dist/visualQa/resolve.js.map +1 -0
- package/dist/visualQa/scene.d.ts +14 -0
- package/dist/visualQa/scene.js +688 -0
- package/dist/visualQa/scene.js.map +1 -0
- package/dist/visualQa/shot.d.ts +2 -0
- package/dist/visualQa/shot.js +27 -0
- package/dist/visualQa/shot.js.map +1 -0
- package/dist/visualQa/signIn.d.ts +35 -0
- package/dist/visualQa/signIn.js +144 -0
- package/dist/visualQa/signIn.js.map +1 -0
- package/dist/visualQa/submitBudget.d.ts +25 -0
- package/dist/visualQa/submitBudget.js +82 -0
- package/dist/visualQa/submitBudget.js.map +1 -0
- package/dist/visualQa/types.d.ts +195 -0
- package/dist/visualQa/types.js +12 -0
- package/dist/visualQa/types.js.map +1 -0
- package/dist/visualQa/vehicle.d.ts +130 -0
- package/dist/visualQa/vehicle.js +2002 -0
- package/dist/visualQa/vehicle.js.map +1 -0
- package/dist/visualQa/vehicleAddonProbe.d.ts +1 -0
- package/dist/visualQa/vehicleAddonProbe.js +115 -0
- package/dist/visualQa/vehicleAddonProbe.js.map +1 -0
- package/dist/visualQa/vehicleCabinFit.d.ts +78 -0
- package/dist/visualQa/vehicleCabinFit.js +55 -0
- package/dist/visualQa/vehicleCabinFit.js.map +1 -0
- package/dist/visualQa/vehicleClearance.d.ts +135 -0
- package/dist/visualQa/vehicleClearance.js +299 -0
- package/dist/visualQa/vehicleClearance.js.map +1 -0
- package/dist/visualQa/vehicleDriveAlive.d.ts +55 -0
- package/dist/visualQa/vehicleDriveAlive.js +103 -0
- package/dist/visualQa/vehicleDriveAlive.js.map +1 -0
- package/dist/visualQa/vehicleGrip.d.ts +156 -0
- package/dist/visualQa/vehicleGrip.js +439 -0
- package/dist/visualQa/vehicleGrip.js.map +1 -0
- package/dist/visualQa/vehicleInspection.d.ts +70 -0
- package/dist/visualQa/vehicleInspection.js +318 -0
- package/dist/visualQa/vehicleInspection.js.map +1 -0
- package/dist/visualQa/vehicleLamps.d.ts +120 -0
- package/dist/visualQa/vehicleLamps.js +377 -0
- package/dist/visualQa/vehicleLamps.js.map +1 -0
- package/dist/visualQa/vehicleMath.d.ts +440 -0
- package/dist/visualQa/vehicleMath.js +967 -0
- package/dist/visualQa/vehicleMath.js.map +1 -0
- package/dist/visualQa/vehicleOccupants.d.ts +66 -0
- package/dist/visualQa/vehicleOccupants.js +448 -0
- package/dist/visualQa/vehicleOccupants.js.map +1 -0
- package/dist/visualQa/vehiclePenetration.d.ts +172 -0
- package/dist/visualQa/vehiclePenetration.js +1157 -0
- package/dist/visualQa/vehiclePenetration.js.map +1 -0
- package/dist/visualQa/vehiclePoses.d.ts +25 -0
- package/dist/visualQa/vehiclePoses.js +56 -0
- package/dist/visualQa/vehiclePoses.js.map +1 -0
- package/dist/visualQa/vehiclePosture.d.ts +78 -0
- package/dist/visualQa/vehiclePosture.js +263 -0
- package/dist/visualQa/vehiclePosture.js.map +1 -0
- package/dist/visualQa/vehiclePreview.d.ts +103 -0
- package/dist/visualQa/vehiclePreview.js +312 -0
- package/dist/visualQa/vehiclePreview.js.map +1 -0
- package/dist/visualQa/vehicleProbe.d.ts +3 -0
- package/dist/visualQa/vehicleProbe.js +670 -0
- package/dist/visualQa/vehicleProbe.js.map +1 -0
- package/dist/visualQa/vehicleStance.d.ts +108 -0
- package/dist/visualQa/vehicleStance.js +200 -0
- package/dist/visualQa/vehicleStance.js.map +1 -0
- package/dist/visualQa/viewer.d.ts +6 -0
- package/dist/visualQa/viewer.js +750 -0
- package/dist/visualQa/viewer.js.map +1 -0
- package/dist/visualQa/viewerGeometry.d.ts +116 -0
- package/dist/visualQa/viewerGeometry.js +476 -0
- package/dist/visualQa/viewerGeometry.js.map +1 -0
- package/dist/visualQa/viewerPage.d.ts +11 -0
- package/dist/visualQa/viewerPage.js +414 -0
- package/dist/visualQa/viewerPage.js.map +1 -0
- package/dist/visualQa/webgpuSelfTest.d.ts +52 -0
- package/dist/visualQa/webgpuSelfTest.js +113 -0
- package/dist/visualQa/webgpuSelfTest.js.map +1 -0
- package/dist/visualQa/winChrome.d.ts +92 -0
- package/dist/visualQa/winChrome.js +445 -0
- package/dist/visualQa/winChrome.js.map +1 -0
- package/dist/visualQa/worldWalk.d.ts +59 -0
- package/dist/visualQa/worldWalk.js +523 -0
- package/dist/visualQa/worldWalk.js.map +1 -0
- package/dist/visualQa/worldWalkMath.d.ts +263 -0
- package/dist/visualQa/worldWalkMath.js +873 -0
- package/dist/visualQa/worldWalkMath.js.map +1 -0
- package/dist/visualQa/worldWalkProbe.d.ts +1 -0
- package/dist/visualQa/worldWalkProbe.js +206 -0
- package/dist/visualQa/worldWalkProbe.js.map +1 -0
- package/dist/visualsConfig.d.ts +65 -0
- package/dist/visualsConfig.js +1333 -0
- package/dist/visualsConfig.js.map +1 -0
- package/dist/voiceCommand.d.ts +11 -0
- package/dist/voiceCommand.js +101 -0
- package/dist/voiceCommand.js.map +1 -0
- package/dist/world/analyze.d.ts +51 -0
- package/dist/world/analyze.js +767 -0
- package/dist/world/analyze.js.map +1 -0
- package/dist/world/baseline.d.ts +33 -0
- package/dist/world/baseline.js +111 -0
- package/dist/world/baseline.js.map +1 -0
- package/dist/world/canonicalize.d.ts +7 -0
- package/dist/world/canonicalize.js +110 -0
- package/dist/world/canonicalize.js.map +1 -0
- package/dist/world/diff.d.ts +18 -0
- package/dist/world/diff.js +58 -0
- package/dist/world/diff.js.map +1 -0
- package/dist/world/focus.d.ts +11 -0
- package/dist/world/focus.js +161 -0
- package/dist/world/focus.js.map +1 -0
- package/dist/world/format.d.ts +6 -0
- package/dist/world/format.js +125 -0
- package/dist/world/format.js.map +1 -0
- package/dist/world/readWorldMetrics.d.ts +10 -0
- package/dist/world/readWorldMetrics.js +31 -0
- package/dist/world/readWorldMetrics.js.map +1 -0
- package/dist/world/snapshotTypes.d.ts +59 -0
- package/dist/world/snapshotTypes.js +8 -0
- package/dist/world/snapshotTypes.js.map +1 -0
- package/dist/world/snapshotWorld.d.ts +16 -0
- package/dist/world/snapshotWorld.js +142 -0
- package/dist/world/snapshotWorld.js.map +1 -0
- package/dist/world/wiring.d.ts +21 -0
- package/dist/world/wiring.js +73 -0
- package/dist/world/wiring.js.map +1 -0
- package/dist/world/zones.d.ts +23 -0
- package/dist/world/zones.js +134 -0
- package/dist/world/zones.js.map +1 -0
- package/docs/bridge.md +304 -0
- package/docs/device-hand-reach.md +44 -0
- package/docs/scene-hlod.md +139 -0
- package/docs/vehicle-audio-import.md +299 -0
- package/docs/vehicle-simulate.md +165 -0
- package/docs/visual-qa-candidate-versions.md +71 -0
- package/docs/visual-qa.md +462 -0
- package/package.json +69 -4
- package/plugins/beamng-vehicle/README.md +129 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_abs.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_audio.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_engine.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_expression.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_jbeam.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_materials.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_stock_events.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/fmod_studio.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/fsb5.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/sim_audio.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_ac.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_beamng.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_fixtures.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/test_beamng_engine.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/test_sim_audio.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/beamng-vehicle.mjs +1295 -0
- package/plugins/beamng-vehicle/bin/beamng_abs.py +408 -0
- package/plugins/beamng-vehicle/bin/beamng_audio.py +394 -0
- package/plugins/beamng-vehicle/bin/beamng_engine.py +184 -0
- package/plugins/beamng-vehicle/bin/beamng_expression.py +373 -0
- package/plugins/beamng-vehicle/bin/beamng_jbeam.py +858 -0
- package/plugins/beamng-vehicle/bin/beamng_materials.py +261 -0
- package/plugins/beamng-vehicle/bin/beamng_stock_events.py +171 -0
- package/plugins/beamng-vehicle/bin/convert-beamng.py +1563 -0
- package/plugins/beamng-vehicle/bin/fmod_studio.py +524 -0
- package/plugins/beamng-vehicle/bin/fsb5.py +304 -0
- package/plugins/beamng-vehicle/bin/index-beamng-common.mjs +16 -0
- package/plugins/beamng-vehicle/bin/index-beamng.py +69 -0
- package/plugins/beamng-vehicle/bin/lower-beamng.py +2399 -0
- package/plugins/beamng-vehicle/bin/sim_audio.py +250 -0
- package/plugins/beamng-vehicle/bin/sim_audio_ac.py +378 -0
- package/plugins/beamng-vehicle/bin/sim_audio_beamng.py +878 -0
- package/plugins/beamng-vehicle/bin/sim_audio_fixtures.py +287 -0
- package/plugins/beamng-vehicle/bin/sim_audio_import.py +135 -0
- package/plugins/beamng-vehicle/bin/test_beamng_engine.py +124 -0
- package/plugins/beamng-vehicle/bin/test_beamng_jbeam.py +383 -0
- package/plugins/beamng-vehicle/bin/test_beamng_precedence.py +134 -0
- package/plugins/beamng-vehicle/bin/test_sim_audio.py +249 -0
- package/plugins/beamng-vehicle/bridge-plugin.json +55 -0
- package/plugins/beamng-vehicle/controller-adapters/beamng-0.39.4-abs.json +52 -0
- package/plugins/beamng-vehicle/lib/common-dependency.mjs +357 -0
- package/plugins/beamng-vehicle/lib/dae-scan.mjs +40 -0
- package/plugins/beamng-vehicle/lib/file-identity.mjs +46 -0
- package/plugins/beamng-vehicle/lib/material-semantics.mjs +312 -0
- package/plugins/beamng-vehicle/lib/palette-contract.mjs +6 -0
- package/plugins/beamng-vehicle/lib/vehicle-qa.mjs +567 -0
- package/plugins/beamng-vehicle/lib/vehicle-source.mjs +247 -0
- package/plugins/beamng-vehicle/profiles/source-compatibility.json +14 -0
- package/plugins/beamng-vehicle/vendor/vehicle-package-v2.mjs +6451 -0
- package/vendor/README.md +63 -0
- package/vendor/ability-manifest.mjs +527 -6
- package/vendor/audits/asset-credits.mjs +96 -0
- package/vendor/audits/check-loaders.mjs +80 -0
- package/vendor/audits/perf-gate.mjs +759 -0
- package/vendor/audits/prove-live.mjs +307 -0
- package/vendor/audits/source-audit.mjs +530 -0
- package/vendor/audits/world-audit.mjs +439 -0
- package/vendor/capture-body.glb +0 -0
- package/vendor/capture-face.glb +0 -0
- package/vendor/installed-index.mjs +1 -1
- 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)
|