From 2ccf17231965477c527771fb921bec11e6f6763a Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 28 Aug 2026 11:44:21 +0300 Subject: [PATCH 1/6] feat(simulation): add Gaussian UGV runtime pipeline --- apps/control-station/package-lock.json | 2 +- apps/control-station/package.json | 1 + .../playcanvas/ammo/2026-08-25/BUILD_INFO.md | 9 + .../playcanvas/ammo/2026-08-25/ammo.wasm.js | 921 ++++++++++++++++ .../playcanvas/ammo/2026-08-25/ammo.wasm.wasm | Bin 0 -> 747690 bytes .../simulation/PlayCanvasRuntime.ts | 277 +++-- .../simulation/SimulationCatalog.tsx | 333 ++++++ .../simulation/SimulationProjectWindow.tsx | 97 +- .../simulation/SimulationUgvController.ts | 999 ++++++++++++++++++ .../simulation/SimulationUgvSettingsPanel.tsx | 191 ++++ .../simulation/SimulationViewport.tsx | 435 +++++--- .../src/core/simulation/projects.ts | 304 +++++- .../src/core/simulation/sourceFiles.ts | 146 +++ apps/control-station/src/productModel.ts | 2 +- .../control-station/src/styles/simulation.css | 265 ++++- .../simulation/SimulationWorkspace.tsx | 310 ++++-- .../test/simulationWorkspace.test.mjs | 141 ++- docs/20_SIMULATION_WORLD_SURFACE_BRIEF.md | 34 +- src/k1link/simulation/projects.py | 467 ++++++-- src/k1link/web/simulation_projects_api.py | 69 +- tests/test_simulation_projects.py | 310 +++++- 21 files changed, 4685 insertions(+), 628 deletions(-) create mode 100644 apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/BUILD_INFO.md create mode 100644 apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/ammo.wasm.js create mode 100644 apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/ammo.wasm.wasm create mode 100644 apps/control-station/src/components/simulation/SimulationCatalog.tsx create mode 100644 apps/control-station/src/components/simulation/SimulationUgvController.ts create mode 100644 apps/control-station/src/components/simulation/SimulationUgvSettingsPanel.tsx create mode 100644 apps/control-station/src/core/simulation/sourceFiles.ts diff --git a/apps/control-station/package-lock.json b/apps/control-station/package-lock.json index 4cf9e16..02ab863 100644 --- a/apps/control-station/package-lock.json +++ b/apps/control-station/package-lock.json @@ -16,6 +16,7 @@ "@nodedc/ui-core": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core", "@nodedc/ui-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", "@rerun-io/web-viewer": "0.34.1", + "meshoptimizer": "1.1.1", "playcanvas": "2.21.4", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -1661,7 +1662,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "dev": true, "license": "MIT" }, "node_modules/ms": { diff --git a/apps/control-station/package.json b/apps/control-station/package.json index b09d50c..edadf39 100644 --- a/apps/control-station/package.json +++ b/apps/control-station/package.json @@ -19,6 +19,7 @@ "@nodedc/ui-core": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core", "@nodedc/ui-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", "@rerun-io/web-viewer": "0.34.1", + "meshoptimizer": "1.1.1", "playcanvas": "2.21.4", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/BUILD_INFO.md b/apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/BUILD_INFO.md new file mode 100644 index 0000000..3544e32 --- /dev/null +++ b/apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/BUILD_INFO.md @@ -0,0 +1,9 @@ +# PlayCanvas Ammo runtime + +- Source: `https://developer.playcanvas.com/assets/modules/ammo/ammo.wasm.js` +- Source: `https://developer.playcanvas.com/assets/modules/ammo/ammo.wasm.wasm` +- Retrieved: 2026-08-27 +- `ammo.wasm.js`: 442459 bytes, SHA-256 `9b765569bce6993e781cd95d548c3711b119f355b512b25722e86a27d6ea8857` +- `ammo.wasm.wasm`: 747690 bytes, SHA-256 `a73b547ed4289aa174bf3424b14b3cb8addc7fe708cd2636d306b72880d6ad13` + +The control station loads these pinned same-origin assets before constructing the PlayCanvas application so the rigid-body systems bind to the expected Ammo instance. diff --git a/apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/ammo.wasm.js b/apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/ammo.wasm.js new file mode 100644 index 0000000..46ab297 --- /dev/null +++ b/apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/ammo.wasm.js @@ -0,0 +1,921 @@ +// This is ammo.js, a port of Bullet Physics to JavaScript. zlib licensed. + +var Ammo = (() => { + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; + if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename; + return ( +function(Ammo = {}) { + +var b;b||(b=typeof Ammo !== 'undefined' ? Ammo : {});var aa,ba;b.ready=new Promise(function(a,c){aa=a;ba=c});var ca=Object.assign({},b),da="object"==typeof window,ea="function"==typeof importScripts,fa="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,ha="",ia,ja,ka; +if(fa){var fs=require("fs"),la=require("path");ha=ea?la.dirname(ha)+"/":__dirname+"/";ia=(a,c)=>{a=a.startsWith("file://")?new URL(a):la.normalize(a);return fs.readFileSync(a,c?void 0:"utf8")};ka=a=>{a=ia(a,!0);a.buffer||(a=new Uint8Array(a));return a};ja=(a,c,d)=>{a=a.startsWith("file://")?new URL(a):la.normalize(a);fs.readFile(a,function(e,g){e?d(e):c(g.buffer)})};1{var c=new XMLHttpRequest;c.open("GET",a,!1);c.send(null);return c.responseText},ea&&(ka=a=>{var c=new XMLHttpRequest;c.open("GET",a,!1);c.responseType="arraybuffer";c.send(null);return new Uint8Array(c.response)}),ja=(a,c,d)=>{var e=new XMLHttpRequest;e.open("GET",a, +!0);e.responseType="arraybuffer";e.onload=()=>{200==e.status||0==e.status&&e.response?c(e.response):d()};e.onerror=d;e.send(null)};b.print||console.log.bind(console);var ma=b.printErr||console.warn.bind(console);Object.assign(b,ca);ca=null;var na;b.wasmBinary&&(na=b.wasmBinary);var noExitRuntime=b.noExitRuntime||!0;"object"!=typeof WebAssembly&&oa("no native wasm support detected");var pa,qa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0; +function sa(a,c){if(a){var d=ta,e=a+c;for(c=a;d[c]&&!(c>=e);)++c;if(16g?e+=String.fromCharCode(g):(g-=65536,e+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else e+=String.fromCharCode(g)}a=e}}else a="";return a} +var ua,ta,va,wa,xa,ya,za=[],Aa=[],Ba=[],Ca=!1;function Ea(){var a=b.preRun.shift();za.unshift(a)}var Fa=0,Ga=null,Ha=null;function oa(a){if(b.onAbort)b.onAbort(a);a="Aborted("+a+")";ma(a);qa=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");ba(a);throw a;}function Ia(a){return a.startsWith("data:application/octet-stream;base64,")}var Ja;Ja="ammo.wasm.wasm";if(!Ia(Ja)){var Ka=Ja;Ja=b.locateFile?b.locateFile(Ka,ha):ha+Ka} +function La(a){try{if(a==Ja&&na)return new Uint8Array(na);if(ka)return ka(a);throw"both async and sync fetching of the wasm failed";}catch(c){oa(c)}} +function Ma(a){if(!na&&(da||ea)){if("function"==typeof fetch&&!a.startsWith("file://"))return fetch(a,{credentials:"same-origin"}).then(function(c){if(!c.ok)throw"failed to load wasm binary file at '"+a+"'";return c.arrayBuffer()}).catch(function(){return La(a)});if(ja)return new Promise(function(c,d){ja(a,function(e){c(new Uint8Array(e))},d)})}return Promise.resolve().then(function(){return La(a)})} +function Na(a,c,d){return Ma(a).then(function(e){return WebAssembly.instantiate(e,c)}).then(function(e){return e}).then(d,function(e){ma("failed to asynchronously prepare wasm: "+e);oa(e)})} +function Oa(a,c){var d=Ja;return na||"function"!=typeof WebAssembly.instantiateStreaming||Ia(d)||d.startsWith("file://")||fa||"function"!=typeof fetch?Na(d,a,c):fetch(d,{credentials:"same-origin"}).then(function(e){return WebAssembly.instantiateStreaming(e,a).then(c,function(g){ma("wasm streaming compile failed: "+g);ma("falling back to ArrayBuffer instantiation");return Na(d,a,c)})})} +var Pa={27338:(a,c,d,e)=>{a=b.getCache(b.DebugDrawer)[a];if(!a.hasOwnProperty("drawLine"))throw"a JSImplementation must implement all functions, you forgot DebugDrawer::drawLine.";a.drawLine(c,d,e)},27558:(a,c,d,e,g,n)=>{a=b.getCache(b.DebugDrawer)[a];if(!a.hasOwnProperty("drawContactPoint"))throw"a JSImplementation must implement all functions, you forgot DebugDrawer::drawContactPoint.";a.drawContactPoint(c,d,e,g,n)},27808:(a,c)=>{a=b.getCache(b.DebugDrawer)[a];if(!a.hasOwnProperty("reportErrorWarning"))throw"a JSImplementation must implement all functions, you forgot DebugDrawer::reportErrorWarning."; +a.reportErrorWarning(c)},28052:(a,c,d)=>{a=b.getCache(b.DebugDrawer)[a];if(!a.hasOwnProperty("draw3dText"))throw"a JSImplementation must implement all functions, you forgot DebugDrawer::draw3dText.";a.draw3dText(c,d)},28275:(a,c)=>{a=b.getCache(b.DebugDrawer)[a];if(!a.hasOwnProperty("setDebugMode"))throw"a JSImplementation must implement all functions, you forgot DebugDrawer::setDebugMode.";a.setDebugMode(c)},28501:a=>{a=b.getCache(b.DebugDrawer)[a];if(!a.hasOwnProperty("getDebugMode"))throw"a JSImplementation must implement all functions, you forgot DebugDrawer::getDebugMode."; +return a.getDebugMode()},28732:(a,c)=>{a=b.getCache(b.MotionState)[a];if(!a.hasOwnProperty("getWorldTransform"))throw"a JSImplementation must implement all functions, you forgot MotionState::getWorldTransform.";a.getWorldTransform(c)},28973:(a,c)=>{a=b.getCache(b.MotionState)[a];if(!a.hasOwnProperty("setWorldTransform"))throw"a JSImplementation must implement all functions, you forgot MotionState::setWorldTransform.";a.setWorldTransform(c)},29214:(a,c,d,e,g,n,z,T)=>{a=b.getCache(b.ConcreteContactResultCallback)[a]; +if(!a.hasOwnProperty("addSingleResult"))throw"a JSImplementation must implement all functions, you forgot ConcreteContactResultCallback::addSingleResult.";return a.addSingleResult(c,d,e,g,n,z,T)}};function Qa(a){for(;0>=2;e=ta[c++];)d+=105!=e&d,Ra.push(105==e?va[d]:xa[d++>>1]),++d;return Pa[a].apply(null,Ra)} +var Ta=[],Ua=void 0,Va=[],Wa={b:function(){oa("")},f:function(a,c,d){return Sa(a,c,d)},a:function(a,c,d){return Sa(a,c,d)},d:function(){return Date.now()},e:function(a,c,d){ta.copyWithin(a,c,c+d)},c:function(){oa("OOM")}}; +(function(){function a(d){d=d.exports;b.asm=d;pa=b.asm.g;var e=pa.buffer;b.HEAP8=ua=new Int8Array(e);b.HEAP16=new Int16Array(e);b.HEAP32=va=new Int32Array(e);b.HEAPU8=ta=new Uint8Array(e);b.HEAPU16=new Uint16Array(e);b.HEAPU32=new Uint32Array(e);b.HEAPF32=wa=new Float32Array(e);b.HEAPF64=xa=new Float64Array(e);ya=b.asm.iB;Aa.unshift(b.asm.h);Fa--;b.monitorRunDependencies&&b.monitorRunDependencies(Fa);0==Fa&&(null!==Ga&&(clearInterval(Ga),Ga=null),Ha&&(e=Ha,Ha=null,e()));return d}var c={a:Wa};Fa++; +b.monitorRunDependencies&&b.monitorRunDependencies(Fa);if(b.instantiateWasm)try{return b.instantiateWasm(c,a)}catch(d){ma("Module.instantiateWasm callback failed with error: "+d),ba(d)}Oa(c,function(d){a(d.instance)}).catch(ba);return{}})(); +var Xa=b._emscripten_bind_btCollisionShape_setLocalScaling_1=function(){return(Xa=b._emscripten_bind_btCollisionShape_setLocalScaling_1=b.asm.i).apply(null,arguments)},Ya=b._emscripten_bind_btCollisionShape_getLocalScaling_0=function(){return(Ya=b._emscripten_bind_btCollisionShape_getLocalScaling_0=b.asm.j).apply(null,arguments)},Za=b._emscripten_bind_btCollisionShape_calculateLocalInertia_2=function(){return(Za=b._emscripten_bind_btCollisionShape_calculateLocalInertia_2=b.asm.k).apply(null,arguments)}, +$a=b._emscripten_bind_btCollisionShape_setMargin_1=function(){return($a=b._emscripten_bind_btCollisionShape_setMargin_1=b.asm.l).apply(null,arguments)},ab=b._emscripten_bind_btCollisionShape_getMargin_0=function(){return(ab=b._emscripten_bind_btCollisionShape_getMargin_0=b.asm.m).apply(null,arguments)},bb=b._emscripten_bind_btCollisionShape___destroy___0=function(){return(bb=b._emscripten_bind_btCollisionShape___destroy___0=b.asm.n).apply(null,arguments)},cb=b._emscripten_bind_btCollisionWorld_getDispatcher_0= +function(){return(cb=b._emscripten_bind_btCollisionWorld_getDispatcher_0=b.asm.o).apply(null,arguments)},db=b._emscripten_bind_btCollisionWorld_rayTest_3=function(){return(db=b._emscripten_bind_btCollisionWorld_rayTest_3=b.asm.p).apply(null,arguments)},eb=b._emscripten_bind_btCollisionWorld_getPairCache_0=function(){return(eb=b._emscripten_bind_btCollisionWorld_getPairCache_0=b.asm.q).apply(null,arguments)},fb=b._emscripten_bind_btCollisionWorld_getDispatchInfo_0=function(){return(fb=b._emscripten_bind_btCollisionWorld_getDispatchInfo_0= +b.asm.r).apply(null,arguments)},gb=b._emscripten_bind_btCollisionWorld_addCollisionObject_1=function(){return(gb=b._emscripten_bind_btCollisionWorld_addCollisionObject_1=b.asm.s).apply(null,arguments)},hb=b._emscripten_bind_btCollisionWorld_addCollisionObject_2=function(){return(hb=b._emscripten_bind_btCollisionWorld_addCollisionObject_2=b.asm.t).apply(null,arguments)},ib=b._emscripten_bind_btCollisionWorld_addCollisionObject_3=function(){return(ib=b._emscripten_bind_btCollisionWorld_addCollisionObject_3= +b.asm.u).apply(null,arguments)},jb=b._emscripten_bind_btCollisionWorld_removeCollisionObject_1=function(){return(jb=b._emscripten_bind_btCollisionWorld_removeCollisionObject_1=b.asm.v).apply(null,arguments)},kb=b._emscripten_bind_btCollisionWorld_getBroadphase_0=function(){return(kb=b._emscripten_bind_btCollisionWorld_getBroadphase_0=b.asm.w).apply(null,arguments)},lb=b._emscripten_bind_btCollisionWorld_convexSweepTest_5=function(){return(lb=b._emscripten_bind_btCollisionWorld_convexSweepTest_5=b.asm.x).apply(null, +arguments)},mb=b._emscripten_bind_btCollisionWorld_contactPairTest_3=function(){return(mb=b._emscripten_bind_btCollisionWorld_contactPairTest_3=b.asm.y).apply(null,arguments)},nb=b._emscripten_bind_btCollisionWorld_contactTest_2=function(){return(nb=b._emscripten_bind_btCollisionWorld_contactTest_2=b.asm.z).apply(null,arguments)},ob=b._emscripten_bind_btCollisionWorld_updateSingleAabb_1=function(){return(ob=b._emscripten_bind_btCollisionWorld_updateSingleAabb_1=b.asm.A).apply(null,arguments)},pb= +b._emscripten_bind_btCollisionWorld_setDebugDrawer_1=function(){return(pb=b._emscripten_bind_btCollisionWorld_setDebugDrawer_1=b.asm.B).apply(null,arguments)},qb=b._emscripten_bind_btCollisionWorld_getDebugDrawer_0=function(){return(qb=b._emscripten_bind_btCollisionWorld_getDebugDrawer_0=b.asm.C).apply(null,arguments)},rb=b._emscripten_bind_btCollisionWorld_debugDrawWorld_0=function(){return(rb=b._emscripten_bind_btCollisionWorld_debugDrawWorld_0=b.asm.D).apply(null,arguments)},sb=b._emscripten_bind_btCollisionWorld_debugDrawObject_3= +function(){return(sb=b._emscripten_bind_btCollisionWorld_debugDrawObject_3=b.asm.E).apply(null,arguments)},tb=b._emscripten_bind_btCollisionWorld___destroy___0=function(){return(tb=b._emscripten_bind_btCollisionWorld___destroy___0=b.asm.F).apply(null,arguments)},ub=b._emscripten_bind_btCollisionObject_setAnisotropicFriction_2=function(){return(ub=b._emscripten_bind_btCollisionObject_setAnisotropicFriction_2=b.asm.G).apply(null,arguments)},vb=b._emscripten_bind_btCollisionObject_getCollisionShape_0= +function(){return(vb=b._emscripten_bind_btCollisionObject_getCollisionShape_0=b.asm.H).apply(null,arguments)},wb=b._emscripten_bind_btCollisionObject_setContactProcessingThreshold_1=function(){return(wb=b._emscripten_bind_btCollisionObject_setContactProcessingThreshold_1=b.asm.I).apply(null,arguments)},xb=b._emscripten_bind_btCollisionObject_setActivationState_1=function(){return(xb=b._emscripten_bind_btCollisionObject_setActivationState_1=b.asm.J).apply(null,arguments)},yb=b._emscripten_bind_btCollisionObject_forceActivationState_1= +function(){return(yb=b._emscripten_bind_btCollisionObject_forceActivationState_1=b.asm.K).apply(null,arguments)},zb=b._emscripten_bind_btCollisionObject_activate_0=function(){return(zb=b._emscripten_bind_btCollisionObject_activate_0=b.asm.L).apply(null,arguments)},Ab=b._emscripten_bind_btCollisionObject_activate_1=function(){return(Ab=b._emscripten_bind_btCollisionObject_activate_1=b.asm.M).apply(null,arguments)},Bb=b._emscripten_bind_btCollisionObject_isActive_0=function(){return(Bb=b._emscripten_bind_btCollisionObject_isActive_0= +b.asm.N).apply(null,arguments)},Cb=b._emscripten_bind_btCollisionObject_isKinematicObject_0=function(){return(Cb=b._emscripten_bind_btCollisionObject_isKinematicObject_0=b.asm.O).apply(null,arguments)},Db=b._emscripten_bind_btCollisionObject_isStaticObject_0=function(){return(Db=b._emscripten_bind_btCollisionObject_isStaticObject_0=b.asm.P).apply(null,arguments)},Eb=b._emscripten_bind_btCollisionObject_isStaticOrKinematicObject_0=function(){return(Eb=b._emscripten_bind_btCollisionObject_isStaticOrKinematicObject_0= +b.asm.Q).apply(null,arguments)},Fb=b._emscripten_bind_btCollisionObject_getRestitution_0=function(){return(Fb=b._emscripten_bind_btCollisionObject_getRestitution_0=b.asm.R).apply(null,arguments)},Gb=b._emscripten_bind_btCollisionObject_getFriction_0=function(){return(Gb=b._emscripten_bind_btCollisionObject_getFriction_0=b.asm.S).apply(null,arguments)},Hb=b._emscripten_bind_btCollisionObject_getRollingFriction_0=function(){return(Hb=b._emscripten_bind_btCollisionObject_getRollingFriction_0=b.asm.T).apply(null, +arguments)},Ib=b._emscripten_bind_btCollisionObject_setRestitution_1=function(){return(Ib=b._emscripten_bind_btCollisionObject_setRestitution_1=b.asm.U).apply(null,arguments)},Jb=b._emscripten_bind_btCollisionObject_setFriction_1=function(){return(Jb=b._emscripten_bind_btCollisionObject_setFriction_1=b.asm.V).apply(null,arguments)},Kb=b._emscripten_bind_btCollisionObject_setRollingFriction_1=function(){return(Kb=b._emscripten_bind_btCollisionObject_setRollingFriction_1=b.asm.W).apply(null,arguments)}, +Lb=b._emscripten_bind_btCollisionObject_getWorldTransform_0=function(){return(Lb=b._emscripten_bind_btCollisionObject_getWorldTransform_0=b.asm.X).apply(null,arguments)},Mb=b._emscripten_bind_btCollisionObject_getCollisionFlags_0=function(){return(Mb=b._emscripten_bind_btCollisionObject_getCollisionFlags_0=b.asm.Y).apply(null,arguments)},Nb=b._emscripten_bind_btCollisionObject_setCollisionFlags_1=function(){return(Nb=b._emscripten_bind_btCollisionObject_setCollisionFlags_1=b.asm.Z).apply(null,arguments)}, +Ob=b._emscripten_bind_btCollisionObject_setWorldTransform_1=function(){return(Ob=b._emscripten_bind_btCollisionObject_setWorldTransform_1=b.asm._).apply(null,arguments)},Pb=b._emscripten_bind_btCollisionObject_setCollisionShape_1=function(){return(Pb=b._emscripten_bind_btCollisionObject_setCollisionShape_1=b.asm.$).apply(null,arguments)},Qb=b._emscripten_bind_btCollisionObject_setCcdMotionThreshold_1=function(){return(Qb=b._emscripten_bind_btCollisionObject_setCcdMotionThreshold_1=b.asm.aa).apply(null, +arguments)},Rb=b._emscripten_bind_btCollisionObject_setCcdSweptSphereRadius_1=function(){return(Rb=b._emscripten_bind_btCollisionObject_setCcdSweptSphereRadius_1=b.asm.ba).apply(null,arguments)},Sb=b._emscripten_bind_btCollisionObject_getUserIndex_0=function(){return(Sb=b._emscripten_bind_btCollisionObject_getUserIndex_0=b.asm.ca).apply(null,arguments)},Tb=b._emscripten_bind_btCollisionObject_setUserIndex_1=function(){return(Tb=b._emscripten_bind_btCollisionObject_setUserIndex_1=b.asm.da).apply(null, +arguments)},Ub=b._emscripten_bind_btCollisionObject_getUserPointer_0=function(){return(Ub=b._emscripten_bind_btCollisionObject_getUserPointer_0=b.asm.ea).apply(null,arguments)},Vb=b._emscripten_bind_btCollisionObject_setUserPointer_1=function(){return(Vb=b._emscripten_bind_btCollisionObject_setUserPointer_1=b.asm.fa).apply(null,arguments)},Wb=b._emscripten_bind_btCollisionObject_getBroadphaseHandle_0=function(){return(Wb=b._emscripten_bind_btCollisionObject_getBroadphaseHandle_0=b.asm.ga).apply(null, +arguments)},Xb=b._emscripten_bind_btCollisionObject___destroy___0=function(){return(Xb=b._emscripten_bind_btCollisionObject___destroy___0=b.asm.ha).apply(null,arguments)},Yb=b._emscripten_bind_btConcaveShape_setLocalScaling_1=function(){return(Yb=b._emscripten_bind_btConcaveShape_setLocalScaling_1=b.asm.ia).apply(null,arguments)},Zb=b._emscripten_bind_btConcaveShape_getLocalScaling_0=function(){return(Zb=b._emscripten_bind_btConcaveShape_getLocalScaling_0=b.asm.ja).apply(null,arguments)},$b=b._emscripten_bind_btConcaveShape_calculateLocalInertia_2= +function(){return($b=b._emscripten_bind_btConcaveShape_calculateLocalInertia_2=b.asm.ka).apply(null,arguments)},ac=b._emscripten_bind_btConcaveShape___destroy___0=function(){return(ac=b._emscripten_bind_btConcaveShape___destroy___0=b.asm.la).apply(null,arguments)},bc=b._emscripten_bind_btCollisionAlgorithm___destroy___0=function(){return(bc=b._emscripten_bind_btCollisionAlgorithm___destroy___0=b.asm.ma).apply(null,arguments)},cc=b._emscripten_bind_btTypedConstraint_enableFeedback_1=function(){return(cc= +b._emscripten_bind_btTypedConstraint_enableFeedback_1=b.asm.na).apply(null,arguments)},ec=b._emscripten_bind_btTypedConstraint_getBreakingImpulseThreshold_0=function(){return(ec=b._emscripten_bind_btTypedConstraint_getBreakingImpulseThreshold_0=b.asm.oa).apply(null,arguments)},fc=b._emscripten_bind_btTypedConstraint_setBreakingImpulseThreshold_1=function(){return(fc=b._emscripten_bind_btTypedConstraint_setBreakingImpulseThreshold_1=b.asm.pa).apply(null,arguments)},gc=b._emscripten_bind_btTypedConstraint_getParam_2= +function(){return(gc=b._emscripten_bind_btTypedConstraint_getParam_2=b.asm.qa).apply(null,arguments)},hc=b._emscripten_bind_btTypedConstraint_setParam_3=function(){return(hc=b._emscripten_bind_btTypedConstraint_setParam_3=b.asm.ra).apply(null,arguments)},ic=b._emscripten_bind_btTypedConstraint___destroy___0=function(){return(ic=b._emscripten_bind_btTypedConstraint___destroy___0=b.asm.sa).apply(null,arguments)},jc=b._emscripten_bind_btDynamicsWorld_addAction_1=function(){return(jc=b._emscripten_bind_btDynamicsWorld_addAction_1= +b.asm.ta).apply(null,arguments)},kc=b._emscripten_bind_btDynamicsWorld_removeAction_1=function(){return(kc=b._emscripten_bind_btDynamicsWorld_removeAction_1=b.asm.ua).apply(null,arguments)},lc=b._emscripten_bind_btDynamicsWorld_getSolverInfo_0=function(){return(lc=b._emscripten_bind_btDynamicsWorld_getSolverInfo_0=b.asm.va).apply(null,arguments)},mc=b._emscripten_bind_btDynamicsWorld_setInternalTickCallback_1=function(){return(mc=b._emscripten_bind_btDynamicsWorld_setInternalTickCallback_1=b.asm.wa).apply(null, +arguments)},nc=b._emscripten_bind_btDynamicsWorld_setInternalTickCallback_2=function(){return(nc=b._emscripten_bind_btDynamicsWorld_setInternalTickCallback_2=b.asm.xa).apply(null,arguments)},oc=b._emscripten_bind_btDynamicsWorld_setInternalTickCallback_3=function(){return(oc=b._emscripten_bind_btDynamicsWorld_setInternalTickCallback_3=b.asm.ya).apply(null,arguments)},pc=b._emscripten_bind_btDynamicsWorld_getDispatcher_0=function(){return(pc=b._emscripten_bind_btDynamicsWorld_getDispatcher_0=b.asm.za).apply(null, +arguments)},qc=b._emscripten_bind_btDynamicsWorld_rayTest_3=function(){return(qc=b._emscripten_bind_btDynamicsWorld_rayTest_3=b.asm.Aa).apply(null,arguments)},rc=b._emscripten_bind_btDynamicsWorld_getPairCache_0=function(){return(rc=b._emscripten_bind_btDynamicsWorld_getPairCache_0=b.asm.Ba).apply(null,arguments)},sc=b._emscripten_bind_btDynamicsWorld_getDispatchInfo_0=function(){return(sc=b._emscripten_bind_btDynamicsWorld_getDispatchInfo_0=b.asm.Ca).apply(null,arguments)},tc=b._emscripten_bind_btDynamicsWorld_addCollisionObject_1= +function(){return(tc=b._emscripten_bind_btDynamicsWorld_addCollisionObject_1=b.asm.Da).apply(null,arguments)},uc=b._emscripten_bind_btDynamicsWorld_addCollisionObject_2=function(){return(uc=b._emscripten_bind_btDynamicsWorld_addCollisionObject_2=b.asm.Ea).apply(null,arguments)},vc=b._emscripten_bind_btDynamicsWorld_addCollisionObject_3=function(){return(vc=b._emscripten_bind_btDynamicsWorld_addCollisionObject_3=b.asm.Fa).apply(null,arguments)},wc=b._emscripten_bind_btDynamicsWorld_removeCollisionObject_1= +function(){return(wc=b._emscripten_bind_btDynamicsWorld_removeCollisionObject_1=b.asm.Ga).apply(null,arguments)},xc=b._emscripten_bind_btDynamicsWorld_getBroadphase_0=function(){return(xc=b._emscripten_bind_btDynamicsWorld_getBroadphase_0=b.asm.Ha).apply(null,arguments)},yc=b._emscripten_bind_btDynamicsWorld_convexSweepTest_5=function(){return(yc=b._emscripten_bind_btDynamicsWorld_convexSweepTest_5=b.asm.Ia).apply(null,arguments)},zc=b._emscripten_bind_btDynamicsWorld_contactPairTest_3=function(){return(zc= +b._emscripten_bind_btDynamicsWorld_contactPairTest_3=b.asm.Ja).apply(null,arguments)},Ac=b._emscripten_bind_btDynamicsWorld_contactTest_2=function(){return(Ac=b._emscripten_bind_btDynamicsWorld_contactTest_2=b.asm.Ka).apply(null,arguments)},Bc=b._emscripten_bind_btDynamicsWorld_updateSingleAabb_1=function(){return(Bc=b._emscripten_bind_btDynamicsWorld_updateSingleAabb_1=b.asm.La).apply(null,arguments)},Cc=b._emscripten_bind_btDynamicsWorld_setDebugDrawer_1=function(){return(Cc=b._emscripten_bind_btDynamicsWorld_setDebugDrawer_1= +b.asm.Ma).apply(null,arguments)},Dc=b._emscripten_bind_btDynamicsWorld_getDebugDrawer_0=function(){return(Dc=b._emscripten_bind_btDynamicsWorld_getDebugDrawer_0=b.asm.Na).apply(null,arguments)},Ec=b._emscripten_bind_btDynamicsWorld_debugDrawWorld_0=function(){return(Ec=b._emscripten_bind_btDynamicsWorld_debugDrawWorld_0=b.asm.Oa).apply(null,arguments)},Fc=b._emscripten_bind_btDynamicsWorld_debugDrawObject_3=function(){return(Fc=b._emscripten_bind_btDynamicsWorld_debugDrawObject_3=b.asm.Pa).apply(null, +arguments)},Gc=b._emscripten_bind_btDynamicsWorld___destroy___0=function(){return(Gc=b._emscripten_bind_btDynamicsWorld___destroy___0=b.asm.Qa).apply(null,arguments)},Hc=b._emscripten_bind_btIDebugDraw_drawLine_3=function(){return(Hc=b._emscripten_bind_btIDebugDraw_drawLine_3=b.asm.Ra).apply(null,arguments)},Ic=b._emscripten_bind_btIDebugDraw_drawContactPoint_5=function(){return(Ic=b._emscripten_bind_btIDebugDraw_drawContactPoint_5=b.asm.Sa).apply(null,arguments)},Jc=b._emscripten_bind_btIDebugDraw_reportErrorWarning_1= +function(){return(Jc=b._emscripten_bind_btIDebugDraw_reportErrorWarning_1=b.asm.Ta).apply(null,arguments)},Kc=b._emscripten_bind_btIDebugDraw_draw3dText_2=function(){return(Kc=b._emscripten_bind_btIDebugDraw_draw3dText_2=b.asm.Ua).apply(null,arguments)},Lc=b._emscripten_bind_btIDebugDraw_setDebugMode_1=function(){return(Lc=b._emscripten_bind_btIDebugDraw_setDebugMode_1=b.asm.Va).apply(null,arguments)},Mc=b._emscripten_bind_btIDebugDraw_getDebugMode_0=function(){return(Mc=b._emscripten_bind_btIDebugDraw_getDebugMode_0= +b.asm.Wa).apply(null,arguments)},Nc=b._emscripten_bind_btIDebugDraw___destroy___0=function(){return(Nc=b._emscripten_bind_btIDebugDraw___destroy___0=b.asm.Xa).apply(null,arguments)},Oc=b._emscripten_bind_btVector3_btVector3_0=function(){return(Oc=b._emscripten_bind_btVector3_btVector3_0=b.asm.Ya).apply(null,arguments)},Pc=b._emscripten_bind_btVector3_btVector3_3=function(){return(Pc=b._emscripten_bind_btVector3_btVector3_3=b.asm.Za).apply(null,arguments)},Qc=b._emscripten_bind_btVector3_length_0= +function(){return(Qc=b._emscripten_bind_btVector3_length_0=b.asm._a).apply(null,arguments)},Rc=b._emscripten_bind_btVector3_x_0=function(){return(Rc=b._emscripten_bind_btVector3_x_0=b.asm.$a).apply(null,arguments)},Sc=b._emscripten_bind_btVector3_y_0=function(){return(Sc=b._emscripten_bind_btVector3_y_0=b.asm.ab).apply(null,arguments)},Tc=b._emscripten_bind_btVector3_z_0=function(){return(Tc=b._emscripten_bind_btVector3_z_0=b.asm.bb).apply(null,arguments)},Uc=b._emscripten_bind_btVector3_setX_1=function(){return(Uc= +b._emscripten_bind_btVector3_setX_1=b.asm.cb).apply(null,arguments)},Vc=b._emscripten_bind_btVector3_setY_1=function(){return(Vc=b._emscripten_bind_btVector3_setY_1=b.asm.db).apply(null,arguments)},Wc=b._emscripten_bind_btVector3_setZ_1=function(){return(Wc=b._emscripten_bind_btVector3_setZ_1=b.asm.eb).apply(null,arguments)},Xc=b._emscripten_bind_btVector3_setValue_3=function(){return(Xc=b._emscripten_bind_btVector3_setValue_3=b.asm.fb).apply(null,arguments)},Yc=b._emscripten_bind_btVector3_normalize_0= +function(){return(Yc=b._emscripten_bind_btVector3_normalize_0=b.asm.gb).apply(null,arguments)},Zc=b._emscripten_bind_btVector3_rotate_2=function(){return(Zc=b._emscripten_bind_btVector3_rotate_2=b.asm.hb).apply(null,arguments)},$c=b._emscripten_bind_btVector3_dot_1=function(){return($c=b._emscripten_bind_btVector3_dot_1=b.asm.ib).apply(null,arguments)},ad=b._emscripten_bind_btVector3_op_mul_1=function(){return(ad=b._emscripten_bind_btVector3_op_mul_1=b.asm.jb).apply(null,arguments)},bd=b._emscripten_bind_btVector3_op_add_1= +function(){return(bd=b._emscripten_bind_btVector3_op_add_1=b.asm.kb).apply(null,arguments)},cd=b._emscripten_bind_btVector3_op_sub_1=function(){return(cd=b._emscripten_bind_btVector3_op_sub_1=b.asm.lb).apply(null,arguments)},dd=b._emscripten_bind_btVector3___destroy___0=function(){return(dd=b._emscripten_bind_btVector3___destroy___0=b.asm.mb).apply(null,arguments)},ed=b._emscripten_bind_btQuadWord_x_0=function(){return(ed=b._emscripten_bind_btQuadWord_x_0=b.asm.nb).apply(null,arguments)},fd=b._emscripten_bind_btQuadWord_y_0= +function(){return(fd=b._emscripten_bind_btQuadWord_y_0=b.asm.ob).apply(null,arguments)},gd=b._emscripten_bind_btQuadWord_z_0=function(){return(gd=b._emscripten_bind_btQuadWord_z_0=b.asm.pb).apply(null,arguments)},hd=b._emscripten_bind_btQuadWord_w_0=function(){return(hd=b._emscripten_bind_btQuadWord_w_0=b.asm.qb).apply(null,arguments)},jd=b._emscripten_bind_btQuadWord_setX_1=function(){return(jd=b._emscripten_bind_btQuadWord_setX_1=b.asm.rb).apply(null,arguments)},kd=b._emscripten_bind_btQuadWord_setY_1= +function(){return(kd=b._emscripten_bind_btQuadWord_setY_1=b.asm.sb).apply(null,arguments)},ld=b._emscripten_bind_btQuadWord_setZ_1=function(){return(ld=b._emscripten_bind_btQuadWord_setZ_1=b.asm.tb).apply(null,arguments)},md=b._emscripten_bind_btQuadWord_setW_1=function(){return(md=b._emscripten_bind_btQuadWord_setW_1=b.asm.ub).apply(null,arguments)},nd=b._emscripten_bind_btQuadWord___destroy___0=function(){return(nd=b._emscripten_bind_btQuadWord___destroy___0=b.asm.vb).apply(null,arguments)},od= +b._emscripten_bind_btMotionState_getWorldTransform_1=function(){return(od=b._emscripten_bind_btMotionState_getWorldTransform_1=b.asm.wb).apply(null,arguments)},pd=b._emscripten_bind_btMotionState_setWorldTransform_1=function(){return(pd=b._emscripten_bind_btMotionState_setWorldTransform_1=b.asm.xb).apply(null,arguments)},qd=b._emscripten_bind_btMotionState___destroy___0=function(){return(qd=b._emscripten_bind_btMotionState___destroy___0=b.asm.yb).apply(null,arguments)},rd=b._emscripten_bind_RayResultCallback_hasHit_0= +function(){return(rd=b._emscripten_bind_RayResultCallback_hasHit_0=b.asm.zb).apply(null,arguments)},sd=b._emscripten_bind_RayResultCallback_get_m_collisionFilterGroup_0=function(){return(sd=b._emscripten_bind_RayResultCallback_get_m_collisionFilterGroup_0=b.asm.Ab).apply(null,arguments)},td=b._emscripten_bind_RayResultCallback_set_m_collisionFilterGroup_1=function(){return(td=b._emscripten_bind_RayResultCallback_set_m_collisionFilterGroup_1=b.asm.Bb).apply(null,arguments)},ud=b._emscripten_bind_RayResultCallback_get_m_collisionFilterMask_0= +function(){return(ud=b._emscripten_bind_RayResultCallback_get_m_collisionFilterMask_0=b.asm.Cb).apply(null,arguments)},vd=b._emscripten_bind_RayResultCallback_set_m_collisionFilterMask_1=function(){return(vd=b._emscripten_bind_RayResultCallback_set_m_collisionFilterMask_1=b.asm.Db).apply(null,arguments)},wd=b._emscripten_bind_RayResultCallback_get_m_closestHitFraction_0=function(){return(wd=b._emscripten_bind_RayResultCallback_get_m_closestHitFraction_0=b.asm.Eb).apply(null,arguments)},xd=b._emscripten_bind_RayResultCallback_set_m_closestHitFraction_1= +function(){return(xd=b._emscripten_bind_RayResultCallback_set_m_closestHitFraction_1=b.asm.Fb).apply(null,arguments)},yd=b._emscripten_bind_RayResultCallback_get_m_collisionObject_0=function(){return(yd=b._emscripten_bind_RayResultCallback_get_m_collisionObject_0=b.asm.Gb).apply(null,arguments)},zd=b._emscripten_bind_RayResultCallback_set_m_collisionObject_1=function(){return(zd=b._emscripten_bind_RayResultCallback_set_m_collisionObject_1=b.asm.Hb).apply(null,arguments)},Ad=b._emscripten_bind_RayResultCallback_get_m_flags_0= +function(){return(Ad=b._emscripten_bind_RayResultCallback_get_m_flags_0=b.asm.Ib).apply(null,arguments)},Bd=b._emscripten_bind_RayResultCallback_set_m_flags_1=function(){return(Bd=b._emscripten_bind_RayResultCallback_set_m_flags_1=b.asm.Jb).apply(null,arguments)},Cd=b._emscripten_bind_RayResultCallback___destroy___0=function(){return(Cd=b._emscripten_bind_RayResultCallback___destroy___0=b.asm.Kb).apply(null,arguments)},Dd=b._emscripten_bind_ContactResultCallback_addSingleResult_7=function(){return(Dd= +b._emscripten_bind_ContactResultCallback_addSingleResult_7=b.asm.Lb).apply(null,arguments)},Ed=b._emscripten_bind_ContactResultCallback___destroy___0=function(){return(Ed=b._emscripten_bind_ContactResultCallback___destroy___0=b.asm.Mb).apply(null,arguments)},Fd=b._emscripten_bind_ConvexResultCallback_hasHit_0=function(){return(Fd=b._emscripten_bind_ConvexResultCallback_hasHit_0=b.asm.Nb).apply(null,arguments)},Gd=b._emscripten_bind_ConvexResultCallback_get_m_collisionFilterGroup_0=function(){return(Gd= +b._emscripten_bind_ConvexResultCallback_get_m_collisionFilterGroup_0=b.asm.Ob).apply(null,arguments)},Hd=b._emscripten_bind_ConvexResultCallback_set_m_collisionFilterGroup_1=function(){return(Hd=b._emscripten_bind_ConvexResultCallback_set_m_collisionFilterGroup_1=b.asm.Pb).apply(null,arguments)},Id=b._emscripten_bind_ConvexResultCallback_get_m_collisionFilterMask_0=function(){return(Id=b._emscripten_bind_ConvexResultCallback_get_m_collisionFilterMask_0=b.asm.Qb).apply(null,arguments)},Jd=b._emscripten_bind_ConvexResultCallback_set_m_collisionFilterMask_1= +function(){return(Jd=b._emscripten_bind_ConvexResultCallback_set_m_collisionFilterMask_1=b.asm.Rb).apply(null,arguments)},Kd=b._emscripten_bind_ConvexResultCallback_get_m_closestHitFraction_0=function(){return(Kd=b._emscripten_bind_ConvexResultCallback_get_m_closestHitFraction_0=b.asm.Sb).apply(null,arguments)},Ld=b._emscripten_bind_ConvexResultCallback_set_m_closestHitFraction_1=function(){return(Ld=b._emscripten_bind_ConvexResultCallback_set_m_closestHitFraction_1=b.asm.Tb).apply(null,arguments)}, +Md=b._emscripten_bind_ConvexResultCallback___destroy___0=function(){return(Md=b._emscripten_bind_ConvexResultCallback___destroy___0=b.asm.Ub).apply(null,arguments)},Nd=b._emscripten_bind_btConvexShape_setLocalScaling_1=function(){return(Nd=b._emscripten_bind_btConvexShape_setLocalScaling_1=b.asm.Vb).apply(null,arguments)},Od=b._emscripten_bind_btConvexShape_getLocalScaling_0=function(){return(Od=b._emscripten_bind_btConvexShape_getLocalScaling_0=b.asm.Wb).apply(null,arguments)},Pd=b._emscripten_bind_btConvexShape_calculateLocalInertia_2= +function(){return(Pd=b._emscripten_bind_btConvexShape_calculateLocalInertia_2=b.asm.Xb).apply(null,arguments)},Qd=b._emscripten_bind_btConvexShape_setMargin_1=function(){return(Qd=b._emscripten_bind_btConvexShape_setMargin_1=b.asm.Yb).apply(null,arguments)},Rd=b._emscripten_bind_btConvexShape_getMargin_0=function(){return(Rd=b._emscripten_bind_btConvexShape_getMargin_0=b.asm.Zb).apply(null,arguments)},Sd=b._emscripten_bind_btConvexShape___destroy___0=function(){return(Sd=b._emscripten_bind_btConvexShape___destroy___0= +b.asm._b).apply(null,arguments)},Td=b._emscripten_bind_btCapsuleShape_btCapsuleShape_2=function(){return(Td=b._emscripten_bind_btCapsuleShape_btCapsuleShape_2=b.asm.$b).apply(null,arguments)},Ud=b._emscripten_bind_btCapsuleShape_setMargin_1=function(){return(Ud=b._emscripten_bind_btCapsuleShape_setMargin_1=b.asm.ac).apply(null,arguments)},Vd=b._emscripten_bind_btCapsuleShape_getMargin_0=function(){return(Vd=b._emscripten_bind_btCapsuleShape_getMargin_0=b.asm.bc).apply(null,arguments)},Wd=b._emscripten_bind_btCapsuleShape_getUpAxis_0= +function(){return(Wd=b._emscripten_bind_btCapsuleShape_getUpAxis_0=b.asm.cc).apply(null,arguments)},Xd=b._emscripten_bind_btCapsuleShape_getRadius_0=function(){return(Xd=b._emscripten_bind_btCapsuleShape_getRadius_0=b.asm.dc).apply(null,arguments)},Yd=b._emscripten_bind_btCapsuleShape_getHalfHeight_0=function(){return(Yd=b._emscripten_bind_btCapsuleShape_getHalfHeight_0=b.asm.ec).apply(null,arguments)},Zd=b._emscripten_bind_btCapsuleShape_setLocalScaling_1=function(){return(Zd=b._emscripten_bind_btCapsuleShape_setLocalScaling_1= +b.asm.fc).apply(null,arguments)},$d=b._emscripten_bind_btCapsuleShape_getLocalScaling_0=function(){return($d=b._emscripten_bind_btCapsuleShape_getLocalScaling_0=b.asm.gc).apply(null,arguments)},ae=b._emscripten_bind_btCapsuleShape_calculateLocalInertia_2=function(){return(ae=b._emscripten_bind_btCapsuleShape_calculateLocalInertia_2=b.asm.hc).apply(null,arguments)},be=b._emscripten_bind_btCapsuleShape___destroy___0=function(){return(be=b._emscripten_bind_btCapsuleShape___destroy___0=b.asm.ic).apply(null, +arguments)},ce=b._emscripten_bind_btCylinderShape_btCylinderShape_1=function(){return(ce=b._emscripten_bind_btCylinderShape_btCylinderShape_1=b.asm.jc).apply(null,arguments)},de=b._emscripten_bind_btCylinderShape_setMargin_1=function(){return(de=b._emscripten_bind_btCylinderShape_setMargin_1=b.asm.kc).apply(null,arguments)},ee=b._emscripten_bind_btCylinderShape_getMargin_0=function(){return(ee=b._emscripten_bind_btCylinderShape_getMargin_0=b.asm.lc).apply(null,arguments)},fe=b._emscripten_bind_btCylinderShape_setLocalScaling_1= +function(){return(fe=b._emscripten_bind_btCylinderShape_setLocalScaling_1=b.asm.mc).apply(null,arguments)},ge=b._emscripten_bind_btCylinderShape_getLocalScaling_0=function(){return(ge=b._emscripten_bind_btCylinderShape_getLocalScaling_0=b.asm.nc).apply(null,arguments)},he=b._emscripten_bind_btCylinderShape_calculateLocalInertia_2=function(){return(he=b._emscripten_bind_btCylinderShape_calculateLocalInertia_2=b.asm.oc).apply(null,arguments)},ie=b._emscripten_bind_btCylinderShape___destroy___0=function(){return(ie= +b._emscripten_bind_btCylinderShape___destroy___0=b.asm.pc).apply(null,arguments)},je=b._emscripten_bind_btConeShape_btConeShape_2=function(){return(je=b._emscripten_bind_btConeShape_btConeShape_2=b.asm.qc).apply(null,arguments)},ke=b._emscripten_bind_btConeShape_setLocalScaling_1=function(){return(ke=b._emscripten_bind_btConeShape_setLocalScaling_1=b.asm.rc).apply(null,arguments)},le=b._emscripten_bind_btConeShape_getLocalScaling_0=function(){return(le=b._emscripten_bind_btConeShape_getLocalScaling_0= +b.asm.sc).apply(null,arguments)},me=b._emscripten_bind_btConeShape_calculateLocalInertia_2=function(){return(me=b._emscripten_bind_btConeShape_calculateLocalInertia_2=b.asm.tc).apply(null,arguments)},ne=b._emscripten_bind_btConeShape___destroy___0=function(){return(ne=b._emscripten_bind_btConeShape___destroy___0=b.asm.uc).apply(null,arguments)},oe=b._emscripten_bind_btStridingMeshInterface_setScaling_1=function(){return(oe=b._emscripten_bind_btStridingMeshInterface_setScaling_1=b.asm.vc).apply(null, +arguments)},pe=b._emscripten_bind_btStridingMeshInterface___destroy___0=function(){return(pe=b._emscripten_bind_btStridingMeshInterface___destroy___0=b.asm.wc).apply(null,arguments)},qe=b._emscripten_bind_btTriangleMeshShape_setLocalScaling_1=function(){return(qe=b._emscripten_bind_btTriangleMeshShape_setLocalScaling_1=b.asm.xc).apply(null,arguments)},re=b._emscripten_bind_btTriangleMeshShape_getLocalScaling_0=function(){return(re=b._emscripten_bind_btTriangleMeshShape_getLocalScaling_0=b.asm.yc).apply(null, +arguments)},se=b._emscripten_bind_btTriangleMeshShape_calculateLocalInertia_2=function(){return(se=b._emscripten_bind_btTriangleMeshShape_calculateLocalInertia_2=b.asm.zc).apply(null,arguments)},te=b._emscripten_bind_btTriangleMeshShape___destroy___0=function(){return(te=b._emscripten_bind_btTriangleMeshShape___destroy___0=b.asm.Ac).apply(null,arguments)},ue=b._emscripten_bind_btPrimitiveManagerBase_is_trimesh_0=function(){return(ue=b._emscripten_bind_btPrimitiveManagerBase_is_trimesh_0=b.asm.Bc).apply(null, +arguments)},ve=b._emscripten_bind_btPrimitiveManagerBase_get_primitive_count_0=function(){return(ve=b._emscripten_bind_btPrimitiveManagerBase_get_primitive_count_0=b.asm.Cc).apply(null,arguments)},we=b._emscripten_bind_btPrimitiveManagerBase_get_primitive_box_2=function(){return(we=b._emscripten_bind_btPrimitiveManagerBase_get_primitive_box_2=b.asm.Dc).apply(null,arguments)},xe=b._emscripten_bind_btPrimitiveManagerBase_get_primitive_triangle_2=function(){return(xe=b._emscripten_bind_btPrimitiveManagerBase_get_primitive_triangle_2= +b.asm.Ec).apply(null,arguments)},ye=b._emscripten_bind_btPrimitiveManagerBase___destroy___0=function(){return(ye=b._emscripten_bind_btPrimitiveManagerBase___destroy___0=b.asm.Fc).apply(null,arguments)},ze=b._emscripten_bind_btGImpactShapeInterface_updateBound_0=function(){return(ze=b._emscripten_bind_btGImpactShapeInterface_updateBound_0=b.asm.Gc).apply(null,arguments)},Ae=b._emscripten_bind_btGImpactShapeInterface_postUpdate_0=function(){return(Ae=b._emscripten_bind_btGImpactShapeInterface_postUpdate_0= +b.asm.Hc).apply(null,arguments)},Be=b._emscripten_bind_btGImpactShapeInterface_getShapeType_0=function(){return(Be=b._emscripten_bind_btGImpactShapeInterface_getShapeType_0=b.asm.Ic).apply(null,arguments)},Ce=b._emscripten_bind_btGImpactShapeInterface_getName_0=function(){return(Ce=b._emscripten_bind_btGImpactShapeInterface_getName_0=b.asm.Jc).apply(null,arguments)},De=b._emscripten_bind_btGImpactShapeInterface_getGImpactShapeType_0=function(){return(De=b._emscripten_bind_btGImpactShapeInterface_getGImpactShapeType_0= +b.asm.Kc).apply(null,arguments)},Ee=b._emscripten_bind_btGImpactShapeInterface_getPrimitiveManager_0=function(){return(Ee=b._emscripten_bind_btGImpactShapeInterface_getPrimitiveManager_0=b.asm.Lc).apply(null,arguments)},Fe=b._emscripten_bind_btGImpactShapeInterface_getNumChildShapes_0=function(){return(Fe=b._emscripten_bind_btGImpactShapeInterface_getNumChildShapes_0=b.asm.Mc).apply(null,arguments)},Ge=b._emscripten_bind_btGImpactShapeInterface_childrenHasTransform_0=function(){return(Ge=b._emscripten_bind_btGImpactShapeInterface_childrenHasTransform_0= +b.asm.Nc).apply(null,arguments)},He=b._emscripten_bind_btGImpactShapeInterface_needsRetrieveTriangles_0=function(){return(He=b._emscripten_bind_btGImpactShapeInterface_needsRetrieveTriangles_0=b.asm.Oc).apply(null,arguments)},Ie=b._emscripten_bind_btGImpactShapeInterface_needsRetrieveTetrahedrons_0=function(){return(Ie=b._emscripten_bind_btGImpactShapeInterface_needsRetrieveTetrahedrons_0=b.asm.Pc).apply(null,arguments)},Je=b._emscripten_bind_btGImpactShapeInterface_getBulletTriangle_2=function(){return(Je= +b._emscripten_bind_btGImpactShapeInterface_getBulletTriangle_2=b.asm.Qc).apply(null,arguments)},Ke=b._emscripten_bind_btGImpactShapeInterface_getBulletTetrahedron_2=function(){return(Ke=b._emscripten_bind_btGImpactShapeInterface_getBulletTetrahedron_2=b.asm.Rc).apply(null,arguments)},Le=b._emscripten_bind_btGImpactShapeInterface_getChildShape_1=function(){return(Le=b._emscripten_bind_btGImpactShapeInterface_getChildShape_1=b.asm.Sc).apply(null,arguments)},Me=b._emscripten_bind_btGImpactShapeInterface_getChildTransform_1= +function(){return(Me=b._emscripten_bind_btGImpactShapeInterface_getChildTransform_1=b.asm.Tc).apply(null,arguments)},Ne=b._emscripten_bind_btGImpactShapeInterface_setChildTransform_2=function(){return(Ne=b._emscripten_bind_btGImpactShapeInterface_setChildTransform_2=b.asm.Uc).apply(null,arguments)},Oe=b._emscripten_bind_btGImpactShapeInterface_setLocalScaling_1=function(){return(Oe=b._emscripten_bind_btGImpactShapeInterface_setLocalScaling_1=b.asm.Vc).apply(null,arguments)},Pe=b._emscripten_bind_btGImpactShapeInterface_getLocalScaling_0= +function(){return(Pe=b._emscripten_bind_btGImpactShapeInterface_getLocalScaling_0=b.asm.Wc).apply(null,arguments)},Qe=b._emscripten_bind_btGImpactShapeInterface_calculateLocalInertia_2=function(){return(Qe=b._emscripten_bind_btGImpactShapeInterface_calculateLocalInertia_2=b.asm.Xc).apply(null,arguments)},Re=b._emscripten_bind_btGImpactShapeInterface___destroy___0=function(){return(Re=b._emscripten_bind_btGImpactShapeInterface___destroy___0=b.asm.Yc).apply(null,arguments)},Se=b._emscripten_bind_btActivatingCollisionAlgorithm___destroy___0= +function(){return(Se=b._emscripten_bind_btActivatingCollisionAlgorithm___destroy___0=b.asm.Zc).apply(null,arguments)},Te=b._emscripten_bind_btDefaultCollisionConfiguration_btDefaultCollisionConfiguration_0=function(){return(Te=b._emscripten_bind_btDefaultCollisionConfiguration_btDefaultCollisionConfiguration_0=b.asm._c).apply(null,arguments)},Ue=b._emscripten_bind_btDefaultCollisionConfiguration_btDefaultCollisionConfiguration_1=function(){return(Ue=b._emscripten_bind_btDefaultCollisionConfiguration_btDefaultCollisionConfiguration_1= +b.asm.$c).apply(null,arguments)},Ve=b._emscripten_bind_btDefaultCollisionConfiguration___destroy___0=function(){return(Ve=b._emscripten_bind_btDefaultCollisionConfiguration___destroy___0=b.asm.ad).apply(null,arguments)},We=b._emscripten_bind_btDispatcher_getNumManifolds_0=function(){return(We=b._emscripten_bind_btDispatcher_getNumManifolds_0=b.asm.bd).apply(null,arguments)},Xe=b._emscripten_bind_btDispatcher_getManifoldByIndexInternal_1=function(){return(Xe=b._emscripten_bind_btDispatcher_getManifoldByIndexInternal_1= +b.asm.cd).apply(null,arguments)},Ye=b._emscripten_bind_btDispatcher___destroy___0=function(){return(Ye=b._emscripten_bind_btDispatcher___destroy___0=b.asm.dd).apply(null,arguments)},Ze=b._emscripten_bind_btGeneric6DofConstraint_btGeneric6DofConstraint_3=function(){return(Ze=b._emscripten_bind_btGeneric6DofConstraint_btGeneric6DofConstraint_3=b.asm.ed).apply(null,arguments)},$e=b._emscripten_bind_btGeneric6DofConstraint_btGeneric6DofConstraint_5=function(){return($e=b._emscripten_bind_btGeneric6DofConstraint_btGeneric6DofConstraint_5= +b.asm.fd).apply(null,arguments)},af=b._emscripten_bind_btGeneric6DofConstraint_setLinearLowerLimit_1=function(){return(af=b._emscripten_bind_btGeneric6DofConstraint_setLinearLowerLimit_1=b.asm.gd).apply(null,arguments)},bf=b._emscripten_bind_btGeneric6DofConstraint_setLinearUpperLimit_1=function(){return(bf=b._emscripten_bind_btGeneric6DofConstraint_setLinearUpperLimit_1=b.asm.hd).apply(null,arguments)},cf=b._emscripten_bind_btGeneric6DofConstraint_setAngularLowerLimit_1=function(){return(cf=b._emscripten_bind_btGeneric6DofConstraint_setAngularLowerLimit_1= +b.asm.id).apply(null,arguments)},df=b._emscripten_bind_btGeneric6DofConstraint_setAngularUpperLimit_1=function(){return(df=b._emscripten_bind_btGeneric6DofConstraint_setAngularUpperLimit_1=b.asm.jd).apply(null,arguments)},ef=b._emscripten_bind_btGeneric6DofConstraint_getFrameOffsetA_0=function(){return(ef=b._emscripten_bind_btGeneric6DofConstraint_getFrameOffsetA_0=b.asm.kd).apply(null,arguments)},ff=b._emscripten_bind_btGeneric6DofConstraint_enableFeedback_1=function(){return(ff=b._emscripten_bind_btGeneric6DofConstraint_enableFeedback_1= +b.asm.ld).apply(null,arguments)},gf=b._emscripten_bind_btGeneric6DofConstraint_getBreakingImpulseThreshold_0=function(){return(gf=b._emscripten_bind_btGeneric6DofConstraint_getBreakingImpulseThreshold_0=b.asm.md).apply(null,arguments)},hf=b._emscripten_bind_btGeneric6DofConstraint_setBreakingImpulseThreshold_1=function(){return(hf=b._emscripten_bind_btGeneric6DofConstraint_setBreakingImpulseThreshold_1=b.asm.nd).apply(null,arguments)},jf=b._emscripten_bind_btGeneric6DofConstraint_getParam_2=function(){return(jf= +b._emscripten_bind_btGeneric6DofConstraint_getParam_2=b.asm.od).apply(null,arguments)},kf=b._emscripten_bind_btGeneric6DofConstraint_setParam_3=function(){return(kf=b._emscripten_bind_btGeneric6DofConstraint_setParam_3=b.asm.pd).apply(null,arguments)},lf=b._emscripten_bind_btGeneric6DofConstraint___destroy___0=function(){return(lf=b._emscripten_bind_btGeneric6DofConstraint___destroy___0=b.asm.qd).apply(null,arguments)},mf=b._emscripten_bind_btDiscreteDynamicsWorld_btDiscreteDynamicsWorld_4=function(){return(mf= +b._emscripten_bind_btDiscreteDynamicsWorld_btDiscreteDynamicsWorld_4=b.asm.rd).apply(null,arguments)},nf=b._emscripten_bind_btDiscreteDynamicsWorld_setGravity_1=function(){return(nf=b._emscripten_bind_btDiscreteDynamicsWorld_setGravity_1=b.asm.sd).apply(null,arguments)},of=b._emscripten_bind_btDiscreteDynamicsWorld_getGravity_0=function(){return(of=b._emscripten_bind_btDiscreteDynamicsWorld_getGravity_0=b.asm.td).apply(null,arguments)},pf=b._emscripten_bind_btDiscreteDynamicsWorld_addRigidBody_1= +function(){return(pf=b._emscripten_bind_btDiscreteDynamicsWorld_addRigidBody_1=b.asm.ud).apply(null,arguments)},qf=b._emscripten_bind_btDiscreteDynamicsWorld_addRigidBody_3=function(){return(qf=b._emscripten_bind_btDiscreteDynamicsWorld_addRigidBody_3=b.asm.vd).apply(null,arguments)},rf=b._emscripten_bind_btDiscreteDynamicsWorld_removeRigidBody_1=function(){return(rf=b._emscripten_bind_btDiscreteDynamicsWorld_removeRigidBody_1=b.asm.wd).apply(null,arguments)},sf=b._emscripten_bind_btDiscreteDynamicsWorld_addConstraint_1= +function(){return(sf=b._emscripten_bind_btDiscreteDynamicsWorld_addConstraint_1=b.asm.xd).apply(null,arguments)},tf=b._emscripten_bind_btDiscreteDynamicsWorld_addConstraint_2=function(){return(tf=b._emscripten_bind_btDiscreteDynamicsWorld_addConstraint_2=b.asm.yd).apply(null,arguments)},uf=b._emscripten_bind_btDiscreteDynamicsWorld_removeConstraint_1=function(){return(uf=b._emscripten_bind_btDiscreteDynamicsWorld_removeConstraint_1=b.asm.zd).apply(null,arguments)},vf=b._emscripten_bind_btDiscreteDynamicsWorld_stepSimulation_1= +function(){return(vf=b._emscripten_bind_btDiscreteDynamicsWorld_stepSimulation_1=b.asm.Ad).apply(null,arguments)},wf=b._emscripten_bind_btDiscreteDynamicsWorld_stepSimulation_2=function(){return(wf=b._emscripten_bind_btDiscreteDynamicsWorld_stepSimulation_2=b.asm.Bd).apply(null,arguments)},xf=b._emscripten_bind_btDiscreteDynamicsWorld_stepSimulation_3=function(){return(xf=b._emscripten_bind_btDiscreteDynamicsWorld_stepSimulation_3=b.asm.Cd).apply(null,arguments)},yf=b._emscripten_bind_btDiscreteDynamicsWorld_setContactAddedCallback_1= +function(){return(yf=b._emscripten_bind_btDiscreteDynamicsWorld_setContactAddedCallback_1=b.asm.Dd).apply(null,arguments)},zf=b._emscripten_bind_btDiscreteDynamicsWorld_setContactProcessedCallback_1=function(){return(zf=b._emscripten_bind_btDiscreteDynamicsWorld_setContactProcessedCallback_1=b.asm.Ed).apply(null,arguments)},Af=b._emscripten_bind_btDiscreteDynamicsWorld_setContactDestroyedCallback_1=function(){return(Af=b._emscripten_bind_btDiscreteDynamicsWorld_setContactDestroyedCallback_1=b.asm.Fd).apply(null, +arguments)},Bf=b._emscripten_bind_btDiscreteDynamicsWorld_getDispatcher_0=function(){return(Bf=b._emscripten_bind_btDiscreteDynamicsWorld_getDispatcher_0=b.asm.Gd).apply(null,arguments)},Cf=b._emscripten_bind_btDiscreteDynamicsWorld_rayTest_3=function(){return(Cf=b._emscripten_bind_btDiscreteDynamicsWorld_rayTest_3=b.asm.Hd).apply(null,arguments)},Df=b._emscripten_bind_btDiscreteDynamicsWorld_getPairCache_0=function(){return(Df=b._emscripten_bind_btDiscreteDynamicsWorld_getPairCache_0=b.asm.Id).apply(null, +arguments)},Ef=b._emscripten_bind_btDiscreteDynamicsWorld_getDispatchInfo_0=function(){return(Ef=b._emscripten_bind_btDiscreteDynamicsWorld_getDispatchInfo_0=b.asm.Jd).apply(null,arguments)},Ff=b._emscripten_bind_btDiscreteDynamicsWorld_addCollisionObject_1=function(){return(Ff=b._emscripten_bind_btDiscreteDynamicsWorld_addCollisionObject_1=b.asm.Kd).apply(null,arguments)},Gf=b._emscripten_bind_btDiscreteDynamicsWorld_addCollisionObject_2=function(){return(Gf=b._emscripten_bind_btDiscreteDynamicsWorld_addCollisionObject_2= +b.asm.Ld).apply(null,arguments)},Hf=b._emscripten_bind_btDiscreteDynamicsWorld_addCollisionObject_3=function(){return(Hf=b._emscripten_bind_btDiscreteDynamicsWorld_addCollisionObject_3=b.asm.Md).apply(null,arguments)},If=b._emscripten_bind_btDiscreteDynamicsWorld_removeCollisionObject_1=function(){return(If=b._emscripten_bind_btDiscreteDynamicsWorld_removeCollisionObject_1=b.asm.Nd).apply(null,arguments)},Jf=b._emscripten_bind_btDiscreteDynamicsWorld_getBroadphase_0=function(){return(Jf=b._emscripten_bind_btDiscreteDynamicsWorld_getBroadphase_0= +b.asm.Od).apply(null,arguments)},Kf=b._emscripten_bind_btDiscreteDynamicsWorld_convexSweepTest_5=function(){return(Kf=b._emscripten_bind_btDiscreteDynamicsWorld_convexSweepTest_5=b.asm.Pd).apply(null,arguments)},Lf=b._emscripten_bind_btDiscreteDynamicsWorld_contactPairTest_3=function(){return(Lf=b._emscripten_bind_btDiscreteDynamicsWorld_contactPairTest_3=b.asm.Qd).apply(null,arguments)},Mf=b._emscripten_bind_btDiscreteDynamicsWorld_contactTest_2=function(){return(Mf=b._emscripten_bind_btDiscreteDynamicsWorld_contactTest_2= +b.asm.Rd).apply(null,arguments)},Nf=b._emscripten_bind_btDiscreteDynamicsWorld_updateSingleAabb_1=function(){return(Nf=b._emscripten_bind_btDiscreteDynamicsWorld_updateSingleAabb_1=b.asm.Sd).apply(null,arguments)},Of=b._emscripten_bind_btDiscreteDynamicsWorld_setDebugDrawer_1=function(){return(Of=b._emscripten_bind_btDiscreteDynamicsWorld_setDebugDrawer_1=b.asm.Td).apply(null,arguments)},Pf=b._emscripten_bind_btDiscreteDynamicsWorld_getDebugDrawer_0=function(){return(Pf=b._emscripten_bind_btDiscreteDynamicsWorld_getDebugDrawer_0= +b.asm.Ud).apply(null,arguments)},Qf=b._emscripten_bind_btDiscreteDynamicsWorld_debugDrawWorld_0=function(){return(Qf=b._emscripten_bind_btDiscreteDynamicsWorld_debugDrawWorld_0=b.asm.Vd).apply(null,arguments)},Rf=b._emscripten_bind_btDiscreteDynamicsWorld_debugDrawObject_3=function(){return(Rf=b._emscripten_bind_btDiscreteDynamicsWorld_debugDrawObject_3=b.asm.Wd).apply(null,arguments)},Sf=b._emscripten_bind_btDiscreteDynamicsWorld_addAction_1=function(){return(Sf=b._emscripten_bind_btDiscreteDynamicsWorld_addAction_1= +b.asm.Xd).apply(null,arguments)},Tf=b._emscripten_bind_btDiscreteDynamicsWorld_removeAction_1=function(){return(Tf=b._emscripten_bind_btDiscreteDynamicsWorld_removeAction_1=b.asm.Yd).apply(null,arguments)},Uf=b._emscripten_bind_btDiscreteDynamicsWorld_getSolverInfo_0=function(){return(Uf=b._emscripten_bind_btDiscreteDynamicsWorld_getSolverInfo_0=b.asm.Zd).apply(null,arguments)},Vf=b._emscripten_bind_btDiscreteDynamicsWorld_setInternalTickCallback_1=function(){return(Vf=b._emscripten_bind_btDiscreteDynamicsWorld_setInternalTickCallback_1= +b.asm._d).apply(null,arguments)},Wf=b._emscripten_bind_btDiscreteDynamicsWorld_setInternalTickCallback_2=function(){return(Wf=b._emscripten_bind_btDiscreteDynamicsWorld_setInternalTickCallback_2=b.asm.$d).apply(null,arguments)},Xf=b._emscripten_bind_btDiscreteDynamicsWorld_setInternalTickCallback_3=function(){return(Xf=b._emscripten_bind_btDiscreteDynamicsWorld_setInternalTickCallback_3=b.asm.ae).apply(null,arguments)},Yf=b._emscripten_bind_btDiscreteDynamicsWorld___destroy___0=function(){return(Yf= +b._emscripten_bind_btDiscreteDynamicsWorld___destroy___0=b.asm.be).apply(null,arguments)},Zf=b._emscripten_bind_btVehicleRaycaster_castRay_3=function(){return(Zf=b._emscripten_bind_btVehicleRaycaster_castRay_3=b.asm.ce).apply(null,arguments)},$f=b._emscripten_bind_btVehicleRaycaster___destroy___0=function(){return($f=b._emscripten_bind_btVehicleRaycaster___destroy___0=b.asm.de).apply(null,arguments)},ag=b._emscripten_bind_btActionInterface_updateAction_2=function(){return(ag=b._emscripten_bind_btActionInterface_updateAction_2= +b.asm.ee).apply(null,arguments)},bg=b._emscripten_bind_btActionInterface___destroy___0=function(){return(bg=b._emscripten_bind_btActionInterface___destroy___0=b.asm.fe).apply(null,arguments)},cg=b._emscripten_bind_btGhostObject_btGhostObject_0=function(){return(cg=b._emscripten_bind_btGhostObject_btGhostObject_0=b.asm.ge).apply(null,arguments)},dg=b._emscripten_bind_btGhostObject_getNumOverlappingObjects_0=function(){return(dg=b._emscripten_bind_btGhostObject_getNumOverlappingObjects_0=b.asm.he).apply(null, +arguments)},eg=b._emscripten_bind_btGhostObject_getOverlappingObject_1=function(){return(eg=b._emscripten_bind_btGhostObject_getOverlappingObject_1=b.asm.ie).apply(null,arguments)},fg=b._emscripten_bind_btGhostObject_setAnisotropicFriction_2=function(){return(fg=b._emscripten_bind_btGhostObject_setAnisotropicFriction_2=b.asm.je).apply(null,arguments)},gg=b._emscripten_bind_btGhostObject_getCollisionShape_0=function(){return(gg=b._emscripten_bind_btGhostObject_getCollisionShape_0=b.asm.ke).apply(null, +arguments)},hg=b._emscripten_bind_btGhostObject_setContactProcessingThreshold_1=function(){return(hg=b._emscripten_bind_btGhostObject_setContactProcessingThreshold_1=b.asm.le).apply(null,arguments)},ig=b._emscripten_bind_btGhostObject_setActivationState_1=function(){return(ig=b._emscripten_bind_btGhostObject_setActivationState_1=b.asm.me).apply(null,arguments)},jg=b._emscripten_bind_btGhostObject_forceActivationState_1=function(){return(jg=b._emscripten_bind_btGhostObject_forceActivationState_1=b.asm.ne).apply(null, +arguments)},kg=b._emscripten_bind_btGhostObject_activate_0=function(){return(kg=b._emscripten_bind_btGhostObject_activate_0=b.asm.oe).apply(null,arguments)},lg=b._emscripten_bind_btGhostObject_activate_1=function(){return(lg=b._emscripten_bind_btGhostObject_activate_1=b.asm.pe).apply(null,arguments)},mg=b._emscripten_bind_btGhostObject_isActive_0=function(){return(mg=b._emscripten_bind_btGhostObject_isActive_0=b.asm.qe).apply(null,arguments)},ng=b._emscripten_bind_btGhostObject_isKinematicObject_0= +function(){return(ng=b._emscripten_bind_btGhostObject_isKinematicObject_0=b.asm.re).apply(null,arguments)},og=b._emscripten_bind_btGhostObject_isStaticObject_0=function(){return(og=b._emscripten_bind_btGhostObject_isStaticObject_0=b.asm.se).apply(null,arguments)},pg=b._emscripten_bind_btGhostObject_isStaticOrKinematicObject_0=function(){return(pg=b._emscripten_bind_btGhostObject_isStaticOrKinematicObject_0=b.asm.te).apply(null,arguments)},qg=b._emscripten_bind_btGhostObject_getRestitution_0=function(){return(qg= +b._emscripten_bind_btGhostObject_getRestitution_0=b.asm.ue).apply(null,arguments)},rg=b._emscripten_bind_btGhostObject_getFriction_0=function(){return(rg=b._emscripten_bind_btGhostObject_getFriction_0=b.asm.ve).apply(null,arguments)},sg=b._emscripten_bind_btGhostObject_getRollingFriction_0=function(){return(sg=b._emscripten_bind_btGhostObject_getRollingFriction_0=b.asm.we).apply(null,arguments)},tg=b._emscripten_bind_btGhostObject_setRestitution_1=function(){return(tg=b._emscripten_bind_btGhostObject_setRestitution_1= +b.asm.xe).apply(null,arguments)},ug=b._emscripten_bind_btGhostObject_setFriction_1=function(){return(ug=b._emscripten_bind_btGhostObject_setFriction_1=b.asm.ye).apply(null,arguments)},vg=b._emscripten_bind_btGhostObject_setRollingFriction_1=function(){return(vg=b._emscripten_bind_btGhostObject_setRollingFriction_1=b.asm.ze).apply(null,arguments)},wg=b._emscripten_bind_btGhostObject_getWorldTransform_0=function(){return(wg=b._emscripten_bind_btGhostObject_getWorldTransform_0=b.asm.Ae).apply(null,arguments)}, +xg=b._emscripten_bind_btGhostObject_getCollisionFlags_0=function(){return(xg=b._emscripten_bind_btGhostObject_getCollisionFlags_0=b.asm.Be).apply(null,arguments)},yg=b._emscripten_bind_btGhostObject_setCollisionFlags_1=function(){return(yg=b._emscripten_bind_btGhostObject_setCollisionFlags_1=b.asm.Ce).apply(null,arguments)},zg=b._emscripten_bind_btGhostObject_setWorldTransform_1=function(){return(zg=b._emscripten_bind_btGhostObject_setWorldTransform_1=b.asm.De).apply(null,arguments)},Ag=b._emscripten_bind_btGhostObject_setCollisionShape_1= +function(){return(Ag=b._emscripten_bind_btGhostObject_setCollisionShape_1=b.asm.Ee).apply(null,arguments)},Bg=b._emscripten_bind_btGhostObject_setCcdMotionThreshold_1=function(){return(Bg=b._emscripten_bind_btGhostObject_setCcdMotionThreshold_1=b.asm.Fe).apply(null,arguments)},Cg=b._emscripten_bind_btGhostObject_setCcdSweptSphereRadius_1=function(){return(Cg=b._emscripten_bind_btGhostObject_setCcdSweptSphereRadius_1=b.asm.Ge).apply(null,arguments)},Dg=b._emscripten_bind_btGhostObject_getUserIndex_0= +function(){return(Dg=b._emscripten_bind_btGhostObject_getUserIndex_0=b.asm.He).apply(null,arguments)},Eg=b._emscripten_bind_btGhostObject_setUserIndex_1=function(){return(Eg=b._emscripten_bind_btGhostObject_setUserIndex_1=b.asm.Ie).apply(null,arguments)},Fg=b._emscripten_bind_btGhostObject_getUserPointer_0=function(){return(Fg=b._emscripten_bind_btGhostObject_getUserPointer_0=b.asm.Je).apply(null,arguments)},Gg=b._emscripten_bind_btGhostObject_setUserPointer_1=function(){return(Gg=b._emscripten_bind_btGhostObject_setUserPointer_1= +b.asm.Ke).apply(null,arguments)},Hg=b._emscripten_bind_btGhostObject_getBroadphaseHandle_0=function(){return(Hg=b._emscripten_bind_btGhostObject_getBroadphaseHandle_0=b.asm.Le).apply(null,arguments)},Ig=b._emscripten_bind_btGhostObject___destroy___0=function(){return(Ig=b._emscripten_bind_btGhostObject___destroy___0=b.asm.Me).apply(null,arguments)},Jg=b._emscripten_bind_btSoftBodySolver___destroy___0=function(){return(Jg=b._emscripten_bind_btSoftBodySolver___destroy___0=b.asm.Ne).apply(null,arguments)}, +Kg=b._emscripten_bind_VoidPtr___destroy___0=function(){return(Kg=b._emscripten_bind_VoidPtr___destroy___0=b.asm.Oe).apply(null,arguments)},Lg=b._emscripten_bind_DebugDrawer_DebugDrawer_0=function(){return(Lg=b._emscripten_bind_DebugDrawer_DebugDrawer_0=b.asm.Pe).apply(null,arguments)},Mg=b._emscripten_bind_DebugDrawer_drawLine_3=function(){return(Mg=b._emscripten_bind_DebugDrawer_drawLine_3=b.asm.Qe).apply(null,arguments)},Ng=b._emscripten_bind_DebugDrawer_drawContactPoint_5=function(){return(Ng= +b._emscripten_bind_DebugDrawer_drawContactPoint_5=b.asm.Re).apply(null,arguments)},Og=b._emscripten_bind_DebugDrawer_reportErrorWarning_1=function(){return(Og=b._emscripten_bind_DebugDrawer_reportErrorWarning_1=b.asm.Se).apply(null,arguments)},Pg=b._emscripten_bind_DebugDrawer_draw3dText_2=function(){return(Pg=b._emscripten_bind_DebugDrawer_draw3dText_2=b.asm.Te).apply(null,arguments)},Qg=b._emscripten_bind_DebugDrawer_setDebugMode_1=function(){return(Qg=b._emscripten_bind_DebugDrawer_setDebugMode_1= +b.asm.Ue).apply(null,arguments)},Rg=b._emscripten_bind_DebugDrawer_getDebugMode_0=function(){return(Rg=b._emscripten_bind_DebugDrawer_getDebugMode_0=b.asm.Ve).apply(null,arguments)},Sg=b._emscripten_bind_DebugDrawer___destroy___0=function(){return(Sg=b._emscripten_bind_DebugDrawer___destroy___0=b.asm.We).apply(null,arguments)},Tg=b._emscripten_bind_btVector4_btVector4_0=function(){return(Tg=b._emscripten_bind_btVector4_btVector4_0=b.asm.Xe).apply(null,arguments)},Ug=b._emscripten_bind_btVector4_btVector4_4= +function(){return(Ug=b._emscripten_bind_btVector4_btVector4_4=b.asm.Ye).apply(null,arguments)},Vg=b._emscripten_bind_btVector4_w_0=function(){return(Vg=b._emscripten_bind_btVector4_w_0=b.asm.Ze).apply(null,arguments)},Wg=b._emscripten_bind_btVector4_setValue_4=function(){return(Wg=b._emscripten_bind_btVector4_setValue_4=b.asm._e).apply(null,arguments)},Xg=b._emscripten_bind_btVector4_length_0=function(){return(Xg=b._emscripten_bind_btVector4_length_0=b.asm.$e).apply(null,arguments)},Yg=b._emscripten_bind_btVector4_x_0= +function(){return(Yg=b._emscripten_bind_btVector4_x_0=b.asm.af).apply(null,arguments)},Zg=b._emscripten_bind_btVector4_y_0=function(){return(Zg=b._emscripten_bind_btVector4_y_0=b.asm.bf).apply(null,arguments)},$g=b._emscripten_bind_btVector4_z_0=function(){return($g=b._emscripten_bind_btVector4_z_0=b.asm.cf).apply(null,arguments)},ah=b._emscripten_bind_btVector4_setX_1=function(){return(ah=b._emscripten_bind_btVector4_setX_1=b.asm.df).apply(null,arguments)},bh=b._emscripten_bind_btVector4_setY_1= +function(){return(bh=b._emscripten_bind_btVector4_setY_1=b.asm.ef).apply(null,arguments)},ch=b._emscripten_bind_btVector4_setZ_1=function(){return(ch=b._emscripten_bind_btVector4_setZ_1=b.asm.ff).apply(null,arguments)},dh=b._emscripten_bind_btVector4_normalize_0=function(){return(dh=b._emscripten_bind_btVector4_normalize_0=b.asm.gf).apply(null,arguments)},eh=b._emscripten_bind_btVector4_rotate_2=function(){return(eh=b._emscripten_bind_btVector4_rotate_2=b.asm.hf).apply(null,arguments)},fh=b._emscripten_bind_btVector4_dot_1= +function(){return(fh=b._emscripten_bind_btVector4_dot_1=b.asm.jf).apply(null,arguments)},gh=b._emscripten_bind_btVector4_op_mul_1=function(){return(gh=b._emscripten_bind_btVector4_op_mul_1=b.asm.kf).apply(null,arguments)},hh=b._emscripten_bind_btVector4_op_add_1=function(){return(hh=b._emscripten_bind_btVector4_op_add_1=b.asm.lf).apply(null,arguments)},ih=b._emscripten_bind_btVector4_op_sub_1=function(){return(ih=b._emscripten_bind_btVector4_op_sub_1=b.asm.mf).apply(null,arguments)},jh=b._emscripten_bind_btVector4___destroy___0= +function(){return(jh=b._emscripten_bind_btVector4___destroy___0=b.asm.nf).apply(null,arguments)},kh=b._emscripten_bind_btQuaternion_btQuaternion_4=function(){return(kh=b._emscripten_bind_btQuaternion_btQuaternion_4=b.asm.of).apply(null,arguments)},lh=b._emscripten_bind_btQuaternion_setValue_4=function(){return(lh=b._emscripten_bind_btQuaternion_setValue_4=b.asm.pf).apply(null,arguments)},mh=b._emscripten_bind_btQuaternion_setEulerZYX_3=function(){return(mh=b._emscripten_bind_btQuaternion_setEulerZYX_3= +b.asm.qf).apply(null,arguments)},nh=b._emscripten_bind_btQuaternion_setRotation_2=function(){return(nh=b._emscripten_bind_btQuaternion_setRotation_2=b.asm.rf).apply(null,arguments)},oh=b._emscripten_bind_btQuaternion_normalize_0=function(){return(oh=b._emscripten_bind_btQuaternion_normalize_0=b.asm.sf).apply(null,arguments)},ph=b._emscripten_bind_btQuaternion_length2_0=function(){return(ph=b._emscripten_bind_btQuaternion_length2_0=b.asm.tf).apply(null,arguments)},qh=b._emscripten_bind_btQuaternion_length_0= +function(){return(qh=b._emscripten_bind_btQuaternion_length_0=b.asm.uf).apply(null,arguments)},rh=b._emscripten_bind_btQuaternion_dot_1=function(){return(rh=b._emscripten_bind_btQuaternion_dot_1=b.asm.vf).apply(null,arguments)},sh=b._emscripten_bind_btQuaternion_normalized_0=function(){return(sh=b._emscripten_bind_btQuaternion_normalized_0=b.asm.wf).apply(null,arguments)},th=b._emscripten_bind_btQuaternion_getAxis_0=function(){return(th=b._emscripten_bind_btQuaternion_getAxis_0=b.asm.xf).apply(null, +arguments)},uh=b._emscripten_bind_btQuaternion_inverse_0=function(){return(uh=b._emscripten_bind_btQuaternion_inverse_0=b.asm.yf).apply(null,arguments)},vh=b._emscripten_bind_btQuaternion_getAngle_0=function(){return(vh=b._emscripten_bind_btQuaternion_getAngle_0=b.asm.zf).apply(null,arguments)},wh=b._emscripten_bind_btQuaternion_getAngleShortestPath_0=function(){return(wh=b._emscripten_bind_btQuaternion_getAngleShortestPath_0=b.asm.Af).apply(null,arguments)},xh=b._emscripten_bind_btQuaternion_angle_1= +function(){return(xh=b._emscripten_bind_btQuaternion_angle_1=b.asm.Bf).apply(null,arguments)},yh=b._emscripten_bind_btQuaternion_angleShortestPath_1=function(){return(yh=b._emscripten_bind_btQuaternion_angleShortestPath_1=b.asm.Cf).apply(null,arguments)},zh=b._emscripten_bind_btQuaternion_op_add_1=function(){return(zh=b._emscripten_bind_btQuaternion_op_add_1=b.asm.Df).apply(null,arguments)},Ah=b._emscripten_bind_btQuaternion_op_sub_1=function(){return(Ah=b._emscripten_bind_btQuaternion_op_sub_1=b.asm.Ef).apply(null, +arguments)},Bh=b._emscripten_bind_btQuaternion_op_mul_1=function(){return(Bh=b._emscripten_bind_btQuaternion_op_mul_1=b.asm.Ff).apply(null,arguments)},Ch=b._emscripten_bind_btQuaternion_op_mulq_1=function(){return(Ch=b._emscripten_bind_btQuaternion_op_mulq_1=b.asm.Gf).apply(null,arguments)},Dh=b._emscripten_bind_btQuaternion_op_div_1=function(){return(Dh=b._emscripten_bind_btQuaternion_op_div_1=b.asm.Hf).apply(null,arguments)},Eh=b._emscripten_bind_btQuaternion_x_0=function(){return(Eh=b._emscripten_bind_btQuaternion_x_0= +b.asm.If).apply(null,arguments)},Fh=b._emscripten_bind_btQuaternion_y_0=function(){return(Fh=b._emscripten_bind_btQuaternion_y_0=b.asm.Jf).apply(null,arguments)},Gh=b._emscripten_bind_btQuaternion_z_0=function(){return(Gh=b._emscripten_bind_btQuaternion_z_0=b.asm.Kf).apply(null,arguments)},Hh=b._emscripten_bind_btQuaternion_w_0=function(){return(Hh=b._emscripten_bind_btQuaternion_w_0=b.asm.Lf).apply(null,arguments)},Ih=b._emscripten_bind_btQuaternion_setX_1=function(){return(Ih=b._emscripten_bind_btQuaternion_setX_1= +b.asm.Mf).apply(null,arguments)},Jh=b._emscripten_bind_btQuaternion_setY_1=function(){return(Jh=b._emscripten_bind_btQuaternion_setY_1=b.asm.Nf).apply(null,arguments)},Kh=b._emscripten_bind_btQuaternion_setZ_1=function(){return(Kh=b._emscripten_bind_btQuaternion_setZ_1=b.asm.Of).apply(null,arguments)},Lh=b._emscripten_bind_btQuaternion_setW_1=function(){return(Lh=b._emscripten_bind_btQuaternion_setW_1=b.asm.Pf).apply(null,arguments)},Mh=b._emscripten_bind_btQuaternion___destroy___0=function(){return(Mh= +b._emscripten_bind_btQuaternion___destroy___0=b.asm.Qf).apply(null,arguments)},Nh=b._emscripten_bind_btMatrix3x3_setEulerZYX_3=function(){return(Nh=b._emscripten_bind_btMatrix3x3_setEulerZYX_3=b.asm.Rf).apply(null,arguments)},Oh=b._emscripten_bind_btMatrix3x3_getRotation_1=function(){return(Oh=b._emscripten_bind_btMatrix3x3_getRotation_1=b.asm.Sf).apply(null,arguments)},Ph=b._emscripten_bind_btMatrix3x3_getRow_1=function(){return(Ph=b._emscripten_bind_btMatrix3x3_getRow_1=b.asm.Tf).apply(null,arguments)}, +Qh=b._emscripten_bind_btMatrix3x3___destroy___0=function(){return(Qh=b._emscripten_bind_btMatrix3x3___destroy___0=b.asm.Uf).apply(null,arguments)},Rh=b._emscripten_bind_btTransform_btTransform_0=function(){return(Rh=b._emscripten_bind_btTransform_btTransform_0=b.asm.Vf).apply(null,arguments)},Sh=b._emscripten_bind_btTransform_btTransform_2=function(){return(Sh=b._emscripten_bind_btTransform_btTransform_2=b.asm.Wf).apply(null,arguments)},Th=b._emscripten_bind_btTransform_setIdentity_0=function(){return(Th= +b._emscripten_bind_btTransform_setIdentity_0=b.asm.Xf).apply(null,arguments)},Uh=b._emscripten_bind_btTransform_setOrigin_1=function(){return(Uh=b._emscripten_bind_btTransform_setOrigin_1=b.asm.Yf).apply(null,arguments)},Vh=b._emscripten_bind_btTransform_setRotation_1=function(){return(Vh=b._emscripten_bind_btTransform_setRotation_1=b.asm.Zf).apply(null,arguments)},Wh=b._emscripten_bind_btTransform_getOrigin_0=function(){return(Wh=b._emscripten_bind_btTransform_getOrigin_0=b.asm._f).apply(null,arguments)}, +Xh=b._emscripten_bind_btTransform_getRotation_0=function(){return(Xh=b._emscripten_bind_btTransform_getRotation_0=b.asm.$f).apply(null,arguments)},Yh=b._emscripten_bind_btTransform_getBasis_0=function(){return(Yh=b._emscripten_bind_btTransform_getBasis_0=b.asm.ag).apply(null,arguments)},Zh=b._emscripten_bind_btTransform_setFromOpenGLMatrix_1=function(){return(Zh=b._emscripten_bind_btTransform_setFromOpenGLMatrix_1=b.asm.bg).apply(null,arguments)},$h=b._emscripten_bind_btTransform_inverse_0=function(){return($h= +b._emscripten_bind_btTransform_inverse_0=b.asm.cg).apply(null,arguments)},ai=b._emscripten_bind_btTransform_op_mul_1=function(){return(ai=b._emscripten_bind_btTransform_op_mul_1=b.asm.dg).apply(null,arguments)},bi=b._emscripten_bind_btTransform___destroy___0=function(){return(bi=b._emscripten_bind_btTransform___destroy___0=b.asm.eg).apply(null,arguments)},ci=b._emscripten_bind_MotionState_MotionState_0=function(){return(ci=b._emscripten_bind_MotionState_MotionState_0=b.asm.fg).apply(null,arguments)}, +di=b._emscripten_bind_MotionState_getWorldTransform_1=function(){return(di=b._emscripten_bind_MotionState_getWorldTransform_1=b.asm.gg).apply(null,arguments)},ei=b._emscripten_bind_MotionState_setWorldTransform_1=function(){return(ei=b._emscripten_bind_MotionState_setWorldTransform_1=b.asm.hg).apply(null,arguments)},fi=b._emscripten_bind_MotionState___destroy___0=function(){return(fi=b._emscripten_bind_MotionState___destroy___0=b.asm.ig).apply(null,arguments)},gi=b._emscripten_bind_btDefaultMotionState_btDefaultMotionState_0= +function(){return(gi=b._emscripten_bind_btDefaultMotionState_btDefaultMotionState_0=b.asm.jg).apply(null,arguments)},hi=b._emscripten_bind_btDefaultMotionState_btDefaultMotionState_1=function(){return(hi=b._emscripten_bind_btDefaultMotionState_btDefaultMotionState_1=b.asm.kg).apply(null,arguments)},ii=b._emscripten_bind_btDefaultMotionState_btDefaultMotionState_2=function(){return(ii=b._emscripten_bind_btDefaultMotionState_btDefaultMotionState_2=b.asm.lg).apply(null,arguments)},ji=b._emscripten_bind_btDefaultMotionState_getWorldTransform_1= +function(){return(ji=b._emscripten_bind_btDefaultMotionState_getWorldTransform_1=b.asm.mg).apply(null,arguments)},ki=b._emscripten_bind_btDefaultMotionState_setWorldTransform_1=function(){return(ki=b._emscripten_bind_btDefaultMotionState_setWorldTransform_1=b.asm.ng).apply(null,arguments)},li=b._emscripten_bind_btDefaultMotionState_get_m_graphicsWorldTrans_0=function(){return(li=b._emscripten_bind_btDefaultMotionState_get_m_graphicsWorldTrans_0=b.asm.og).apply(null,arguments)},mi=b._emscripten_bind_btDefaultMotionState_set_m_graphicsWorldTrans_1= +function(){return(mi=b._emscripten_bind_btDefaultMotionState_set_m_graphicsWorldTrans_1=b.asm.pg).apply(null,arguments)},ni=b._emscripten_bind_btDefaultMotionState___destroy___0=function(){return(ni=b._emscripten_bind_btDefaultMotionState___destroy___0=b.asm.qg).apply(null,arguments)},oi=b._emscripten_bind_btCollisionObjectWrapper_getWorldTransform_0=function(){return(oi=b._emscripten_bind_btCollisionObjectWrapper_getWorldTransform_0=b.asm.rg).apply(null,arguments)},pi=b._emscripten_bind_btCollisionObjectWrapper_getCollisionObject_0= +function(){return(pi=b._emscripten_bind_btCollisionObjectWrapper_getCollisionObject_0=b.asm.sg).apply(null,arguments)},qi=b._emscripten_bind_btCollisionObjectWrapper_getCollisionShape_0=function(){return(qi=b._emscripten_bind_btCollisionObjectWrapper_getCollisionShape_0=b.asm.tg).apply(null,arguments)},ri=b._emscripten_bind_ClosestRayResultCallback_ClosestRayResultCallback_2=function(){return(ri=b._emscripten_bind_ClosestRayResultCallback_ClosestRayResultCallback_2=b.asm.ug).apply(null,arguments)}, +si=b._emscripten_bind_ClosestRayResultCallback_hasHit_0=function(){return(si=b._emscripten_bind_ClosestRayResultCallback_hasHit_0=b.asm.vg).apply(null,arguments)},ti=b._emscripten_bind_ClosestRayResultCallback_get_m_rayFromWorld_0=function(){return(ti=b._emscripten_bind_ClosestRayResultCallback_get_m_rayFromWorld_0=b.asm.wg).apply(null,arguments)},ui=b._emscripten_bind_ClosestRayResultCallback_set_m_rayFromWorld_1=function(){return(ui=b._emscripten_bind_ClosestRayResultCallback_set_m_rayFromWorld_1= +b.asm.xg).apply(null,arguments)},vi=b._emscripten_bind_ClosestRayResultCallback_get_m_rayToWorld_0=function(){return(vi=b._emscripten_bind_ClosestRayResultCallback_get_m_rayToWorld_0=b.asm.yg).apply(null,arguments)},wi=b._emscripten_bind_ClosestRayResultCallback_set_m_rayToWorld_1=function(){return(wi=b._emscripten_bind_ClosestRayResultCallback_set_m_rayToWorld_1=b.asm.zg).apply(null,arguments)},xi=b._emscripten_bind_ClosestRayResultCallback_get_m_hitNormalWorld_0=function(){return(xi=b._emscripten_bind_ClosestRayResultCallback_get_m_hitNormalWorld_0= +b.asm.Ag).apply(null,arguments)},yi=b._emscripten_bind_ClosestRayResultCallback_set_m_hitNormalWorld_1=function(){return(yi=b._emscripten_bind_ClosestRayResultCallback_set_m_hitNormalWorld_1=b.asm.Bg).apply(null,arguments)},zi=b._emscripten_bind_ClosestRayResultCallback_get_m_hitPointWorld_0=function(){return(zi=b._emscripten_bind_ClosestRayResultCallback_get_m_hitPointWorld_0=b.asm.Cg).apply(null,arguments)},Ai=b._emscripten_bind_ClosestRayResultCallback_set_m_hitPointWorld_1=function(){return(Ai= +b._emscripten_bind_ClosestRayResultCallback_set_m_hitPointWorld_1=b.asm.Dg).apply(null,arguments)},Bi=b._emscripten_bind_ClosestRayResultCallback_get_m_collisionFilterGroup_0=function(){return(Bi=b._emscripten_bind_ClosestRayResultCallback_get_m_collisionFilterGroup_0=b.asm.Eg).apply(null,arguments)},Ci=b._emscripten_bind_ClosestRayResultCallback_set_m_collisionFilterGroup_1=function(){return(Ci=b._emscripten_bind_ClosestRayResultCallback_set_m_collisionFilterGroup_1=b.asm.Fg).apply(null,arguments)}, +Di=b._emscripten_bind_ClosestRayResultCallback_get_m_collisionFilterMask_0=function(){return(Di=b._emscripten_bind_ClosestRayResultCallback_get_m_collisionFilterMask_0=b.asm.Gg).apply(null,arguments)},Ei=b._emscripten_bind_ClosestRayResultCallback_set_m_collisionFilterMask_1=function(){return(Ei=b._emscripten_bind_ClosestRayResultCallback_set_m_collisionFilterMask_1=b.asm.Hg).apply(null,arguments)},Fi=b._emscripten_bind_ClosestRayResultCallback_get_m_closestHitFraction_0=function(){return(Fi=b._emscripten_bind_ClosestRayResultCallback_get_m_closestHitFraction_0= +b.asm.Ig).apply(null,arguments)},Gi=b._emscripten_bind_ClosestRayResultCallback_set_m_closestHitFraction_1=function(){return(Gi=b._emscripten_bind_ClosestRayResultCallback_set_m_closestHitFraction_1=b.asm.Jg).apply(null,arguments)},Hi=b._emscripten_bind_ClosestRayResultCallback_get_m_collisionObject_0=function(){return(Hi=b._emscripten_bind_ClosestRayResultCallback_get_m_collisionObject_0=b.asm.Kg).apply(null,arguments)},Ii=b._emscripten_bind_ClosestRayResultCallback_set_m_collisionObject_1=function(){return(Ii= +b._emscripten_bind_ClosestRayResultCallback_set_m_collisionObject_1=b.asm.Lg).apply(null,arguments)},Ji=b._emscripten_bind_ClosestRayResultCallback_get_m_flags_0=function(){return(Ji=b._emscripten_bind_ClosestRayResultCallback_get_m_flags_0=b.asm.Mg).apply(null,arguments)},Ki=b._emscripten_bind_ClosestRayResultCallback_set_m_flags_1=function(){return(Ki=b._emscripten_bind_ClosestRayResultCallback_set_m_flags_1=b.asm.Ng).apply(null,arguments)},Li=b._emscripten_bind_ClosestRayResultCallback___destroy___0= +function(){return(Li=b._emscripten_bind_ClosestRayResultCallback___destroy___0=b.asm.Og).apply(null,arguments)},Mi=b._emscripten_bind_btConstCollisionObjectArray_size_0=function(){return(Mi=b._emscripten_bind_btConstCollisionObjectArray_size_0=b.asm.Pg).apply(null,arguments)},Ni=b._emscripten_bind_btConstCollisionObjectArray_at_1=function(){return(Ni=b._emscripten_bind_btConstCollisionObjectArray_at_1=b.asm.Qg).apply(null,arguments)},Oi=b._emscripten_bind_btConstCollisionObjectArray___destroy___0= +function(){return(Oi=b._emscripten_bind_btConstCollisionObjectArray___destroy___0=b.asm.Rg).apply(null,arguments)},Pi=b._emscripten_bind_btScalarArray_size_0=function(){return(Pi=b._emscripten_bind_btScalarArray_size_0=b.asm.Sg).apply(null,arguments)},Qi=b._emscripten_bind_btScalarArray_at_1=function(){return(Qi=b._emscripten_bind_btScalarArray_at_1=b.asm.Tg).apply(null,arguments)},Ri=b._emscripten_bind_btScalarArray___destroy___0=function(){return(Ri=b._emscripten_bind_btScalarArray___destroy___0= +b.asm.Ug).apply(null,arguments)},Si=b._emscripten_bind_AllHitsRayResultCallback_AllHitsRayResultCallback_2=function(){return(Si=b._emscripten_bind_AllHitsRayResultCallback_AllHitsRayResultCallback_2=b.asm.Vg).apply(null,arguments)},Ti=b._emscripten_bind_AllHitsRayResultCallback_hasHit_0=function(){return(Ti=b._emscripten_bind_AllHitsRayResultCallback_hasHit_0=b.asm.Wg).apply(null,arguments)},Ui=b._emscripten_bind_AllHitsRayResultCallback_get_m_collisionObjects_0=function(){return(Ui=b._emscripten_bind_AllHitsRayResultCallback_get_m_collisionObjects_0= +b.asm.Xg).apply(null,arguments)},Vi=b._emscripten_bind_AllHitsRayResultCallback_set_m_collisionObjects_1=function(){return(Vi=b._emscripten_bind_AllHitsRayResultCallback_set_m_collisionObjects_1=b.asm.Yg).apply(null,arguments)},Wi=b._emscripten_bind_AllHitsRayResultCallback_get_m_rayFromWorld_0=function(){return(Wi=b._emscripten_bind_AllHitsRayResultCallback_get_m_rayFromWorld_0=b.asm.Zg).apply(null,arguments)},Xi=b._emscripten_bind_AllHitsRayResultCallback_set_m_rayFromWorld_1=function(){return(Xi= +b._emscripten_bind_AllHitsRayResultCallback_set_m_rayFromWorld_1=b.asm._g).apply(null,arguments)},Yi=b._emscripten_bind_AllHitsRayResultCallback_get_m_rayToWorld_0=function(){return(Yi=b._emscripten_bind_AllHitsRayResultCallback_get_m_rayToWorld_0=b.asm.$g).apply(null,arguments)},Zi=b._emscripten_bind_AllHitsRayResultCallback_set_m_rayToWorld_1=function(){return(Zi=b._emscripten_bind_AllHitsRayResultCallback_set_m_rayToWorld_1=b.asm.ah).apply(null,arguments)},$i=b._emscripten_bind_AllHitsRayResultCallback_get_m_hitNormalWorld_0= +function(){return($i=b._emscripten_bind_AllHitsRayResultCallback_get_m_hitNormalWorld_0=b.asm.bh).apply(null,arguments)},aj=b._emscripten_bind_AllHitsRayResultCallback_set_m_hitNormalWorld_1=function(){return(aj=b._emscripten_bind_AllHitsRayResultCallback_set_m_hitNormalWorld_1=b.asm.ch).apply(null,arguments)},bj=b._emscripten_bind_AllHitsRayResultCallback_get_m_hitPointWorld_0=function(){return(bj=b._emscripten_bind_AllHitsRayResultCallback_get_m_hitPointWorld_0=b.asm.dh).apply(null,arguments)}, +cj=b._emscripten_bind_AllHitsRayResultCallback_set_m_hitPointWorld_1=function(){return(cj=b._emscripten_bind_AllHitsRayResultCallback_set_m_hitPointWorld_1=b.asm.eh).apply(null,arguments)},dj=b._emscripten_bind_AllHitsRayResultCallback_get_m_hitFractions_0=function(){return(dj=b._emscripten_bind_AllHitsRayResultCallback_get_m_hitFractions_0=b.asm.fh).apply(null,arguments)},ej=b._emscripten_bind_AllHitsRayResultCallback_set_m_hitFractions_1=function(){return(ej=b._emscripten_bind_AllHitsRayResultCallback_set_m_hitFractions_1= +b.asm.gh).apply(null,arguments)},fj=b._emscripten_bind_AllHitsRayResultCallback_get_m_collisionFilterGroup_0=function(){return(fj=b._emscripten_bind_AllHitsRayResultCallback_get_m_collisionFilterGroup_0=b.asm.hh).apply(null,arguments)},gj=b._emscripten_bind_AllHitsRayResultCallback_set_m_collisionFilterGroup_1=function(){return(gj=b._emscripten_bind_AllHitsRayResultCallback_set_m_collisionFilterGroup_1=b.asm.ih).apply(null,arguments)},hj=b._emscripten_bind_AllHitsRayResultCallback_get_m_collisionFilterMask_0= +function(){return(hj=b._emscripten_bind_AllHitsRayResultCallback_get_m_collisionFilterMask_0=b.asm.jh).apply(null,arguments)},ij=b._emscripten_bind_AllHitsRayResultCallback_set_m_collisionFilterMask_1=function(){return(ij=b._emscripten_bind_AllHitsRayResultCallback_set_m_collisionFilterMask_1=b.asm.kh).apply(null,arguments)},jj=b._emscripten_bind_AllHitsRayResultCallback_get_m_closestHitFraction_0=function(){return(jj=b._emscripten_bind_AllHitsRayResultCallback_get_m_closestHitFraction_0=b.asm.lh).apply(null, +arguments)},kj=b._emscripten_bind_AllHitsRayResultCallback_set_m_closestHitFraction_1=function(){return(kj=b._emscripten_bind_AllHitsRayResultCallback_set_m_closestHitFraction_1=b.asm.mh).apply(null,arguments)},lj=b._emscripten_bind_AllHitsRayResultCallback_get_m_collisionObject_0=function(){return(lj=b._emscripten_bind_AllHitsRayResultCallback_get_m_collisionObject_0=b.asm.nh).apply(null,arguments)},mj=b._emscripten_bind_AllHitsRayResultCallback_set_m_collisionObject_1=function(){return(mj=b._emscripten_bind_AllHitsRayResultCallback_set_m_collisionObject_1= +b.asm.oh).apply(null,arguments)},nj=b._emscripten_bind_AllHitsRayResultCallback_get_m_flags_0=function(){return(nj=b._emscripten_bind_AllHitsRayResultCallback_get_m_flags_0=b.asm.ph).apply(null,arguments)},oj=b._emscripten_bind_AllHitsRayResultCallback_set_m_flags_1=function(){return(oj=b._emscripten_bind_AllHitsRayResultCallback_set_m_flags_1=b.asm.qh).apply(null,arguments)},pj=b._emscripten_bind_AllHitsRayResultCallback___destroy___0=function(){return(pj=b._emscripten_bind_AllHitsRayResultCallback___destroy___0= +b.asm.rh).apply(null,arguments)},qj=b._emscripten_bind_btManifoldPoint_getPositionWorldOnA_0=function(){return(qj=b._emscripten_bind_btManifoldPoint_getPositionWorldOnA_0=b.asm.sh).apply(null,arguments)},rj=b._emscripten_bind_btManifoldPoint_getPositionWorldOnB_0=function(){return(rj=b._emscripten_bind_btManifoldPoint_getPositionWorldOnB_0=b.asm.th).apply(null,arguments)},sj=b._emscripten_bind_btManifoldPoint_getAppliedImpulse_0=function(){return(sj=b._emscripten_bind_btManifoldPoint_getAppliedImpulse_0= +b.asm.uh).apply(null,arguments)},tj=b._emscripten_bind_btManifoldPoint_getDistance_0=function(){return(tj=b._emscripten_bind_btManifoldPoint_getDistance_0=b.asm.vh).apply(null,arguments)},uj=b._emscripten_bind_btManifoldPoint_get_m_localPointA_0=function(){return(uj=b._emscripten_bind_btManifoldPoint_get_m_localPointA_0=b.asm.wh).apply(null,arguments)},vj=b._emscripten_bind_btManifoldPoint_set_m_localPointA_1=function(){return(vj=b._emscripten_bind_btManifoldPoint_set_m_localPointA_1=b.asm.xh).apply(null, +arguments)},wj=b._emscripten_bind_btManifoldPoint_get_m_localPointB_0=function(){return(wj=b._emscripten_bind_btManifoldPoint_get_m_localPointB_0=b.asm.yh).apply(null,arguments)},xj=b._emscripten_bind_btManifoldPoint_set_m_localPointB_1=function(){return(xj=b._emscripten_bind_btManifoldPoint_set_m_localPointB_1=b.asm.zh).apply(null,arguments)},yj=b._emscripten_bind_btManifoldPoint_get_m_positionWorldOnB_0=function(){return(yj=b._emscripten_bind_btManifoldPoint_get_m_positionWorldOnB_0=b.asm.Ah).apply(null, +arguments)},zj=b._emscripten_bind_btManifoldPoint_set_m_positionWorldOnB_1=function(){return(zj=b._emscripten_bind_btManifoldPoint_set_m_positionWorldOnB_1=b.asm.Bh).apply(null,arguments)},Aj=b._emscripten_bind_btManifoldPoint_get_m_positionWorldOnA_0=function(){return(Aj=b._emscripten_bind_btManifoldPoint_get_m_positionWorldOnA_0=b.asm.Ch).apply(null,arguments)},Bj=b._emscripten_bind_btManifoldPoint_set_m_positionWorldOnA_1=function(){return(Bj=b._emscripten_bind_btManifoldPoint_set_m_positionWorldOnA_1= +b.asm.Dh).apply(null,arguments)},Cj=b._emscripten_bind_btManifoldPoint_get_m_normalWorldOnB_0=function(){return(Cj=b._emscripten_bind_btManifoldPoint_get_m_normalWorldOnB_0=b.asm.Eh).apply(null,arguments)},Dj=b._emscripten_bind_btManifoldPoint_set_m_normalWorldOnB_1=function(){return(Dj=b._emscripten_bind_btManifoldPoint_set_m_normalWorldOnB_1=b.asm.Fh).apply(null,arguments)},Ej=b._emscripten_bind_btManifoldPoint_get_m_userPersistentData_0=function(){return(Ej=b._emscripten_bind_btManifoldPoint_get_m_userPersistentData_0= +b.asm.Gh).apply(null,arguments)},Fj=b._emscripten_bind_btManifoldPoint_set_m_userPersistentData_1=function(){return(Fj=b._emscripten_bind_btManifoldPoint_set_m_userPersistentData_1=b.asm.Hh).apply(null,arguments)},Gj=b._emscripten_bind_btManifoldPoint___destroy___0=function(){return(Gj=b._emscripten_bind_btManifoldPoint___destroy___0=b.asm.Ih).apply(null,arguments)},Hj=b._emscripten_bind_ConcreteContactResultCallback_ConcreteContactResultCallback_0=function(){return(Hj=b._emscripten_bind_ConcreteContactResultCallback_ConcreteContactResultCallback_0= +b.asm.Jh).apply(null,arguments)},Ij=b._emscripten_bind_ConcreteContactResultCallback_addSingleResult_7=function(){return(Ij=b._emscripten_bind_ConcreteContactResultCallback_addSingleResult_7=b.asm.Kh).apply(null,arguments)},Jj=b._emscripten_bind_ConcreteContactResultCallback___destroy___0=function(){return(Jj=b._emscripten_bind_ConcreteContactResultCallback___destroy___0=b.asm.Lh).apply(null,arguments)},Kj=b._emscripten_bind_LocalShapeInfo_get_m_shapePart_0=function(){return(Kj=b._emscripten_bind_LocalShapeInfo_get_m_shapePart_0= +b.asm.Mh).apply(null,arguments)},Lj=b._emscripten_bind_LocalShapeInfo_set_m_shapePart_1=function(){return(Lj=b._emscripten_bind_LocalShapeInfo_set_m_shapePart_1=b.asm.Nh).apply(null,arguments)},Mj=b._emscripten_bind_LocalShapeInfo_get_m_triangleIndex_0=function(){return(Mj=b._emscripten_bind_LocalShapeInfo_get_m_triangleIndex_0=b.asm.Oh).apply(null,arguments)},Nj=b._emscripten_bind_LocalShapeInfo_set_m_triangleIndex_1=function(){return(Nj=b._emscripten_bind_LocalShapeInfo_set_m_triangleIndex_1=b.asm.Ph).apply(null, +arguments)},Oj=b._emscripten_bind_LocalShapeInfo___destroy___0=function(){return(Oj=b._emscripten_bind_LocalShapeInfo___destroy___0=b.asm.Qh).apply(null,arguments)},Pj=b._emscripten_bind_LocalConvexResult_LocalConvexResult_5=function(){return(Pj=b._emscripten_bind_LocalConvexResult_LocalConvexResult_5=b.asm.Rh).apply(null,arguments)},Qj=b._emscripten_bind_LocalConvexResult_get_m_hitCollisionObject_0=function(){return(Qj=b._emscripten_bind_LocalConvexResult_get_m_hitCollisionObject_0=b.asm.Sh).apply(null, +arguments)},Rj=b._emscripten_bind_LocalConvexResult_set_m_hitCollisionObject_1=function(){return(Rj=b._emscripten_bind_LocalConvexResult_set_m_hitCollisionObject_1=b.asm.Th).apply(null,arguments)},Sj=b._emscripten_bind_LocalConvexResult_get_m_localShapeInfo_0=function(){return(Sj=b._emscripten_bind_LocalConvexResult_get_m_localShapeInfo_0=b.asm.Uh).apply(null,arguments)},Tj=b._emscripten_bind_LocalConvexResult_set_m_localShapeInfo_1=function(){return(Tj=b._emscripten_bind_LocalConvexResult_set_m_localShapeInfo_1= +b.asm.Vh).apply(null,arguments)},Uj=b._emscripten_bind_LocalConvexResult_get_m_hitNormalLocal_0=function(){return(Uj=b._emscripten_bind_LocalConvexResult_get_m_hitNormalLocal_0=b.asm.Wh).apply(null,arguments)},Vj=b._emscripten_bind_LocalConvexResult_set_m_hitNormalLocal_1=function(){return(Vj=b._emscripten_bind_LocalConvexResult_set_m_hitNormalLocal_1=b.asm.Xh).apply(null,arguments)},Wj=b._emscripten_bind_LocalConvexResult_get_m_hitPointLocal_0=function(){return(Wj=b._emscripten_bind_LocalConvexResult_get_m_hitPointLocal_0= +b.asm.Yh).apply(null,arguments)},Xj=b._emscripten_bind_LocalConvexResult_set_m_hitPointLocal_1=function(){return(Xj=b._emscripten_bind_LocalConvexResult_set_m_hitPointLocal_1=b.asm.Zh).apply(null,arguments)},Yj=b._emscripten_bind_LocalConvexResult_get_m_hitFraction_0=function(){return(Yj=b._emscripten_bind_LocalConvexResult_get_m_hitFraction_0=b.asm._h).apply(null,arguments)},Zj=b._emscripten_bind_LocalConvexResult_set_m_hitFraction_1=function(){return(Zj=b._emscripten_bind_LocalConvexResult_set_m_hitFraction_1= +b.asm.$h).apply(null,arguments)},ak=b._emscripten_bind_LocalConvexResult___destroy___0=function(){return(ak=b._emscripten_bind_LocalConvexResult___destroy___0=b.asm.ai).apply(null,arguments)},bk=b._emscripten_bind_ClosestConvexResultCallback_ClosestConvexResultCallback_2=function(){return(bk=b._emscripten_bind_ClosestConvexResultCallback_ClosestConvexResultCallback_2=b.asm.bi).apply(null,arguments)},ck=b._emscripten_bind_ClosestConvexResultCallback_hasHit_0=function(){return(ck=b._emscripten_bind_ClosestConvexResultCallback_hasHit_0= +b.asm.ci).apply(null,arguments)},dk=b._emscripten_bind_ClosestConvexResultCallback_get_m_hitCollisionObject_0=function(){return(dk=b._emscripten_bind_ClosestConvexResultCallback_get_m_hitCollisionObject_0=b.asm.di).apply(null,arguments)},ek=b._emscripten_bind_ClosestConvexResultCallback_set_m_hitCollisionObject_1=function(){return(ek=b._emscripten_bind_ClosestConvexResultCallback_set_m_hitCollisionObject_1=b.asm.ei).apply(null,arguments)},fk=b._emscripten_bind_ClosestConvexResultCallback_get_m_convexFromWorld_0= +function(){return(fk=b._emscripten_bind_ClosestConvexResultCallback_get_m_convexFromWorld_0=b.asm.fi).apply(null,arguments)},gk=b._emscripten_bind_ClosestConvexResultCallback_set_m_convexFromWorld_1=function(){return(gk=b._emscripten_bind_ClosestConvexResultCallback_set_m_convexFromWorld_1=b.asm.gi).apply(null,arguments)},hk=b._emscripten_bind_ClosestConvexResultCallback_get_m_convexToWorld_0=function(){return(hk=b._emscripten_bind_ClosestConvexResultCallback_get_m_convexToWorld_0=b.asm.hi).apply(null, +arguments)},ik=b._emscripten_bind_ClosestConvexResultCallback_set_m_convexToWorld_1=function(){return(ik=b._emscripten_bind_ClosestConvexResultCallback_set_m_convexToWorld_1=b.asm.ii).apply(null,arguments)},jk=b._emscripten_bind_ClosestConvexResultCallback_get_m_hitNormalWorld_0=function(){return(jk=b._emscripten_bind_ClosestConvexResultCallback_get_m_hitNormalWorld_0=b.asm.ji).apply(null,arguments)},kk=b._emscripten_bind_ClosestConvexResultCallback_set_m_hitNormalWorld_1=function(){return(kk=b._emscripten_bind_ClosestConvexResultCallback_set_m_hitNormalWorld_1= +b.asm.ki).apply(null,arguments)},lk=b._emscripten_bind_ClosestConvexResultCallback_get_m_hitPointWorld_0=function(){return(lk=b._emscripten_bind_ClosestConvexResultCallback_get_m_hitPointWorld_0=b.asm.li).apply(null,arguments)},mk=b._emscripten_bind_ClosestConvexResultCallback_set_m_hitPointWorld_1=function(){return(mk=b._emscripten_bind_ClosestConvexResultCallback_set_m_hitPointWorld_1=b.asm.mi).apply(null,arguments)},nk=b._emscripten_bind_ClosestConvexResultCallback_get_m_collisionFilterGroup_0= +function(){return(nk=b._emscripten_bind_ClosestConvexResultCallback_get_m_collisionFilterGroup_0=b.asm.ni).apply(null,arguments)},ok=b._emscripten_bind_ClosestConvexResultCallback_set_m_collisionFilterGroup_1=function(){return(ok=b._emscripten_bind_ClosestConvexResultCallback_set_m_collisionFilterGroup_1=b.asm.oi).apply(null,arguments)},pk=b._emscripten_bind_ClosestConvexResultCallback_get_m_collisionFilterMask_0=function(){return(pk=b._emscripten_bind_ClosestConvexResultCallback_get_m_collisionFilterMask_0= +b.asm.pi).apply(null,arguments)},qk=b._emscripten_bind_ClosestConvexResultCallback_set_m_collisionFilterMask_1=function(){return(qk=b._emscripten_bind_ClosestConvexResultCallback_set_m_collisionFilterMask_1=b.asm.qi).apply(null,arguments)},rk=b._emscripten_bind_ClosestConvexResultCallback_get_m_closestHitFraction_0=function(){return(rk=b._emscripten_bind_ClosestConvexResultCallback_get_m_closestHitFraction_0=b.asm.ri).apply(null,arguments)},sk=b._emscripten_bind_ClosestConvexResultCallback_set_m_closestHitFraction_1= +function(){return(sk=b._emscripten_bind_ClosestConvexResultCallback_set_m_closestHitFraction_1=b.asm.si).apply(null,arguments)},tk=b._emscripten_bind_ClosestConvexResultCallback___destroy___0=function(){return(tk=b._emscripten_bind_ClosestConvexResultCallback___destroy___0=b.asm.ti).apply(null,arguments)},uk=b._emscripten_bind_btConvexTriangleMeshShape_btConvexTriangleMeshShape_1=function(){return(uk=b._emscripten_bind_btConvexTriangleMeshShape_btConvexTriangleMeshShape_1=b.asm.ui).apply(null,arguments)}, +vk=b._emscripten_bind_btConvexTriangleMeshShape_btConvexTriangleMeshShape_2=function(){return(vk=b._emscripten_bind_btConvexTriangleMeshShape_btConvexTriangleMeshShape_2=b.asm.vi).apply(null,arguments)},wk=b._emscripten_bind_btConvexTriangleMeshShape_setLocalScaling_1=function(){return(wk=b._emscripten_bind_btConvexTriangleMeshShape_setLocalScaling_1=b.asm.wi).apply(null,arguments)},xk=b._emscripten_bind_btConvexTriangleMeshShape_getLocalScaling_0=function(){return(xk=b._emscripten_bind_btConvexTriangleMeshShape_getLocalScaling_0= +b.asm.xi).apply(null,arguments)},yk=b._emscripten_bind_btConvexTriangleMeshShape_calculateLocalInertia_2=function(){return(yk=b._emscripten_bind_btConvexTriangleMeshShape_calculateLocalInertia_2=b.asm.yi).apply(null,arguments)},zk=b._emscripten_bind_btConvexTriangleMeshShape_setMargin_1=function(){return(zk=b._emscripten_bind_btConvexTriangleMeshShape_setMargin_1=b.asm.zi).apply(null,arguments)},Ak=b._emscripten_bind_btConvexTriangleMeshShape_getMargin_0=function(){return(Ak=b._emscripten_bind_btConvexTriangleMeshShape_getMargin_0= +b.asm.Ai).apply(null,arguments)},Bk=b._emscripten_bind_btConvexTriangleMeshShape___destroy___0=function(){return(Bk=b._emscripten_bind_btConvexTriangleMeshShape___destroy___0=b.asm.Bi).apply(null,arguments)},Ck=b._emscripten_bind_btBoxShape_btBoxShape_1=function(){return(Ck=b._emscripten_bind_btBoxShape_btBoxShape_1=b.asm.Ci).apply(null,arguments)},Dk=b._emscripten_bind_btBoxShape_setMargin_1=function(){return(Dk=b._emscripten_bind_btBoxShape_setMargin_1=b.asm.Di).apply(null,arguments)},Ek=b._emscripten_bind_btBoxShape_getMargin_0= +function(){return(Ek=b._emscripten_bind_btBoxShape_getMargin_0=b.asm.Ei).apply(null,arguments)},Fk=b._emscripten_bind_btBoxShape_setLocalScaling_1=function(){return(Fk=b._emscripten_bind_btBoxShape_setLocalScaling_1=b.asm.Fi).apply(null,arguments)},Gk=b._emscripten_bind_btBoxShape_getLocalScaling_0=function(){return(Gk=b._emscripten_bind_btBoxShape_getLocalScaling_0=b.asm.Gi).apply(null,arguments)},Hk=b._emscripten_bind_btBoxShape_calculateLocalInertia_2=function(){return(Hk=b._emscripten_bind_btBoxShape_calculateLocalInertia_2= +b.asm.Hi).apply(null,arguments)},Ik=b._emscripten_bind_btBoxShape___destroy___0=function(){return(Ik=b._emscripten_bind_btBoxShape___destroy___0=b.asm.Ii).apply(null,arguments)},Jk=b._emscripten_bind_btCapsuleShapeX_btCapsuleShapeX_2=function(){return(Jk=b._emscripten_bind_btCapsuleShapeX_btCapsuleShapeX_2=b.asm.Ji).apply(null,arguments)},Kk=b._emscripten_bind_btCapsuleShapeX_setMargin_1=function(){return(Kk=b._emscripten_bind_btCapsuleShapeX_setMargin_1=b.asm.Ki).apply(null,arguments)},Lk=b._emscripten_bind_btCapsuleShapeX_getMargin_0= +function(){return(Lk=b._emscripten_bind_btCapsuleShapeX_getMargin_0=b.asm.Li).apply(null,arguments)},Mk=b._emscripten_bind_btCapsuleShapeX_getUpAxis_0=function(){return(Mk=b._emscripten_bind_btCapsuleShapeX_getUpAxis_0=b.asm.Mi).apply(null,arguments)},Nk=b._emscripten_bind_btCapsuleShapeX_getRadius_0=function(){return(Nk=b._emscripten_bind_btCapsuleShapeX_getRadius_0=b.asm.Ni).apply(null,arguments)},Ok=b._emscripten_bind_btCapsuleShapeX_getHalfHeight_0=function(){return(Ok=b._emscripten_bind_btCapsuleShapeX_getHalfHeight_0= +b.asm.Oi).apply(null,arguments)},Pk=b._emscripten_bind_btCapsuleShapeX_setLocalScaling_1=function(){return(Pk=b._emscripten_bind_btCapsuleShapeX_setLocalScaling_1=b.asm.Pi).apply(null,arguments)},Qk=b._emscripten_bind_btCapsuleShapeX_getLocalScaling_0=function(){return(Qk=b._emscripten_bind_btCapsuleShapeX_getLocalScaling_0=b.asm.Qi).apply(null,arguments)},Rk=b._emscripten_bind_btCapsuleShapeX_calculateLocalInertia_2=function(){return(Rk=b._emscripten_bind_btCapsuleShapeX_calculateLocalInertia_2= +b.asm.Ri).apply(null,arguments)},Sk=b._emscripten_bind_btCapsuleShapeX___destroy___0=function(){return(Sk=b._emscripten_bind_btCapsuleShapeX___destroy___0=b.asm.Si).apply(null,arguments)},Tk=b._emscripten_bind_btCapsuleShapeZ_btCapsuleShapeZ_2=function(){return(Tk=b._emscripten_bind_btCapsuleShapeZ_btCapsuleShapeZ_2=b.asm.Ti).apply(null,arguments)},Uk=b._emscripten_bind_btCapsuleShapeZ_setMargin_1=function(){return(Uk=b._emscripten_bind_btCapsuleShapeZ_setMargin_1=b.asm.Ui).apply(null,arguments)}, +Vk=b._emscripten_bind_btCapsuleShapeZ_getMargin_0=function(){return(Vk=b._emscripten_bind_btCapsuleShapeZ_getMargin_0=b.asm.Vi).apply(null,arguments)},Wk=b._emscripten_bind_btCapsuleShapeZ_getUpAxis_0=function(){return(Wk=b._emscripten_bind_btCapsuleShapeZ_getUpAxis_0=b.asm.Wi).apply(null,arguments)},Xk=b._emscripten_bind_btCapsuleShapeZ_getRadius_0=function(){return(Xk=b._emscripten_bind_btCapsuleShapeZ_getRadius_0=b.asm.Xi).apply(null,arguments)},Yk=b._emscripten_bind_btCapsuleShapeZ_getHalfHeight_0= +function(){return(Yk=b._emscripten_bind_btCapsuleShapeZ_getHalfHeight_0=b.asm.Yi).apply(null,arguments)},Zk=b._emscripten_bind_btCapsuleShapeZ_setLocalScaling_1=function(){return(Zk=b._emscripten_bind_btCapsuleShapeZ_setLocalScaling_1=b.asm.Zi).apply(null,arguments)},$k=b._emscripten_bind_btCapsuleShapeZ_getLocalScaling_0=function(){return($k=b._emscripten_bind_btCapsuleShapeZ_getLocalScaling_0=b.asm._i).apply(null,arguments)},al=b._emscripten_bind_btCapsuleShapeZ_calculateLocalInertia_2=function(){return(al= +b._emscripten_bind_btCapsuleShapeZ_calculateLocalInertia_2=b.asm.$i).apply(null,arguments)},bl=b._emscripten_bind_btCapsuleShapeZ___destroy___0=function(){return(bl=b._emscripten_bind_btCapsuleShapeZ___destroy___0=b.asm.aj).apply(null,arguments)},cl=b._emscripten_bind_btCylinderShapeX_btCylinderShapeX_1=function(){return(cl=b._emscripten_bind_btCylinderShapeX_btCylinderShapeX_1=b.asm.bj).apply(null,arguments)},dl=b._emscripten_bind_btCylinderShapeX_setMargin_1=function(){return(dl=b._emscripten_bind_btCylinderShapeX_setMargin_1= +b.asm.cj).apply(null,arguments)},el=b._emscripten_bind_btCylinderShapeX_getMargin_0=function(){return(el=b._emscripten_bind_btCylinderShapeX_getMargin_0=b.asm.dj).apply(null,arguments)},fl=b._emscripten_bind_btCylinderShapeX_setLocalScaling_1=function(){return(fl=b._emscripten_bind_btCylinderShapeX_setLocalScaling_1=b.asm.ej).apply(null,arguments)},gl=b._emscripten_bind_btCylinderShapeX_getLocalScaling_0=function(){return(gl=b._emscripten_bind_btCylinderShapeX_getLocalScaling_0=b.asm.fj).apply(null, +arguments)},hl=b._emscripten_bind_btCylinderShapeX_calculateLocalInertia_2=function(){return(hl=b._emscripten_bind_btCylinderShapeX_calculateLocalInertia_2=b.asm.gj).apply(null,arguments)},il=b._emscripten_bind_btCylinderShapeX___destroy___0=function(){return(il=b._emscripten_bind_btCylinderShapeX___destroy___0=b.asm.hj).apply(null,arguments)},jl=b._emscripten_bind_btCylinderShapeZ_btCylinderShapeZ_1=function(){return(jl=b._emscripten_bind_btCylinderShapeZ_btCylinderShapeZ_1=b.asm.ij).apply(null, +arguments)},kl=b._emscripten_bind_btCylinderShapeZ_setMargin_1=function(){return(kl=b._emscripten_bind_btCylinderShapeZ_setMargin_1=b.asm.jj).apply(null,arguments)},ll=b._emscripten_bind_btCylinderShapeZ_getMargin_0=function(){return(ll=b._emscripten_bind_btCylinderShapeZ_getMargin_0=b.asm.kj).apply(null,arguments)},ml=b._emscripten_bind_btCylinderShapeZ_setLocalScaling_1=function(){return(ml=b._emscripten_bind_btCylinderShapeZ_setLocalScaling_1=b.asm.lj).apply(null,arguments)},nl=b._emscripten_bind_btCylinderShapeZ_getLocalScaling_0= +function(){return(nl=b._emscripten_bind_btCylinderShapeZ_getLocalScaling_0=b.asm.mj).apply(null,arguments)},ol=b._emscripten_bind_btCylinderShapeZ_calculateLocalInertia_2=function(){return(ol=b._emscripten_bind_btCylinderShapeZ_calculateLocalInertia_2=b.asm.nj).apply(null,arguments)},pl=b._emscripten_bind_btCylinderShapeZ___destroy___0=function(){return(pl=b._emscripten_bind_btCylinderShapeZ___destroy___0=b.asm.oj).apply(null,arguments)},ql=b._emscripten_bind_btSphereShape_btSphereShape_1=function(){return(ql= +b._emscripten_bind_btSphereShape_btSphereShape_1=b.asm.pj).apply(null,arguments)},rl=b._emscripten_bind_btSphereShape_setMargin_1=function(){return(rl=b._emscripten_bind_btSphereShape_setMargin_1=b.asm.qj).apply(null,arguments)},sl=b._emscripten_bind_btSphereShape_getMargin_0=function(){return(sl=b._emscripten_bind_btSphereShape_getMargin_0=b.asm.rj).apply(null,arguments)},tl=b._emscripten_bind_btSphereShape_setLocalScaling_1=function(){return(tl=b._emscripten_bind_btSphereShape_setLocalScaling_1= +b.asm.sj).apply(null,arguments)},ul=b._emscripten_bind_btSphereShape_getLocalScaling_0=function(){return(ul=b._emscripten_bind_btSphereShape_getLocalScaling_0=b.asm.tj).apply(null,arguments)},vl=b._emscripten_bind_btSphereShape_calculateLocalInertia_2=function(){return(vl=b._emscripten_bind_btSphereShape_calculateLocalInertia_2=b.asm.uj).apply(null,arguments)},wl=b._emscripten_bind_btSphereShape___destroy___0=function(){return(wl=b._emscripten_bind_btSphereShape___destroy___0=b.asm.vj).apply(null, +arguments)},xl=b._emscripten_bind_btMultiSphereShape_btMultiSphereShape_3=function(){return(xl=b._emscripten_bind_btMultiSphereShape_btMultiSphereShape_3=b.asm.wj).apply(null,arguments)},yl=b._emscripten_bind_btMultiSphereShape_setLocalScaling_1=function(){return(yl=b._emscripten_bind_btMultiSphereShape_setLocalScaling_1=b.asm.xj).apply(null,arguments)},zl=b._emscripten_bind_btMultiSphereShape_getLocalScaling_0=function(){return(zl=b._emscripten_bind_btMultiSphereShape_getLocalScaling_0=b.asm.yj).apply(null, +arguments)},Al=b._emscripten_bind_btMultiSphereShape_calculateLocalInertia_2=function(){return(Al=b._emscripten_bind_btMultiSphereShape_calculateLocalInertia_2=b.asm.zj).apply(null,arguments)},Bl=b._emscripten_bind_btMultiSphereShape___destroy___0=function(){return(Bl=b._emscripten_bind_btMultiSphereShape___destroy___0=b.asm.Aj).apply(null,arguments)},Cl=b._emscripten_bind_btConeShapeX_btConeShapeX_2=function(){return(Cl=b._emscripten_bind_btConeShapeX_btConeShapeX_2=b.asm.Bj).apply(null,arguments)}, +Dl=b._emscripten_bind_btConeShapeX_setLocalScaling_1=function(){return(Dl=b._emscripten_bind_btConeShapeX_setLocalScaling_1=b.asm.Cj).apply(null,arguments)},El=b._emscripten_bind_btConeShapeX_getLocalScaling_0=function(){return(El=b._emscripten_bind_btConeShapeX_getLocalScaling_0=b.asm.Dj).apply(null,arguments)},Fl=b._emscripten_bind_btConeShapeX_calculateLocalInertia_2=function(){return(Fl=b._emscripten_bind_btConeShapeX_calculateLocalInertia_2=b.asm.Ej).apply(null,arguments)},Gl=b._emscripten_bind_btConeShapeX___destroy___0= +function(){return(Gl=b._emscripten_bind_btConeShapeX___destroy___0=b.asm.Fj).apply(null,arguments)},Hl=b._emscripten_bind_btConeShapeZ_btConeShapeZ_2=function(){return(Hl=b._emscripten_bind_btConeShapeZ_btConeShapeZ_2=b.asm.Gj).apply(null,arguments)},Il=b._emscripten_bind_btConeShapeZ_setLocalScaling_1=function(){return(Il=b._emscripten_bind_btConeShapeZ_setLocalScaling_1=b.asm.Hj).apply(null,arguments)},Jl=b._emscripten_bind_btConeShapeZ_getLocalScaling_0=function(){return(Jl=b._emscripten_bind_btConeShapeZ_getLocalScaling_0= +b.asm.Ij).apply(null,arguments)},Kl=b._emscripten_bind_btConeShapeZ_calculateLocalInertia_2=function(){return(Kl=b._emscripten_bind_btConeShapeZ_calculateLocalInertia_2=b.asm.Jj).apply(null,arguments)},Ll=b._emscripten_bind_btConeShapeZ___destroy___0=function(){return(Ll=b._emscripten_bind_btConeShapeZ___destroy___0=b.asm.Kj).apply(null,arguments)},Ml=b._emscripten_bind_btIntArray_size_0=function(){return(Ml=b._emscripten_bind_btIntArray_size_0=b.asm.Lj).apply(null,arguments)},Nl=b._emscripten_bind_btIntArray_at_1= +function(){return(Nl=b._emscripten_bind_btIntArray_at_1=b.asm.Mj).apply(null,arguments)},Ol=b._emscripten_bind_btIntArray___destroy___0=function(){return(Ol=b._emscripten_bind_btIntArray___destroy___0=b.asm.Nj).apply(null,arguments)},Pl=b._emscripten_bind_btFace_get_m_indices_0=function(){return(Pl=b._emscripten_bind_btFace_get_m_indices_0=b.asm.Oj).apply(null,arguments)},Ql=b._emscripten_bind_btFace_set_m_indices_1=function(){return(Ql=b._emscripten_bind_btFace_set_m_indices_1=b.asm.Pj).apply(null, +arguments)},Rl=b._emscripten_bind_btFace_get_m_plane_1=function(){return(Rl=b._emscripten_bind_btFace_get_m_plane_1=b.asm.Qj).apply(null,arguments)},Sl=b._emscripten_bind_btFace_set_m_plane_2=function(){return(Sl=b._emscripten_bind_btFace_set_m_plane_2=b.asm.Rj).apply(null,arguments)},Tl=b._emscripten_bind_btFace___destroy___0=function(){return(Tl=b._emscripten_bind_btFace___destroy___0=b.asm.Sj).apply(null,arguments)},Ul=b._emscripten_bind_btVector3Array_size_0=function(){return(Ul=b._emscripten_bind_btVector3Array_size_0= +b.asm.Tj).apply(null,arguments)},Vl=b._emscripten_bind_btVector3Array_at_1=function(){return(Vl=b._emscripten_bind_btVector3Array_at_1=b.asm.Uj).apply(null,arguments)},Wl=b._emscripten_bind_btVector3Array___destroy___0=function(){return(Wl=b._emscripten_bind_btVector3Array___destroy___0=b.asm.Vj).apply(null,arguments)},Xl=b._emscripten_bind_btFaceArray_size_0=function(){return(Xl=b._emscripten_bind_btFaceArray_size_0=b.asm.Wj).apply(null,arguments)},Yl=b._emscripten_bind_btFaceArray_at_1=function(){return(Yl= +b._emscripten_bind_btFaceArray_at_1=b.asm.Xj).apply(null,arguments)},Zl=b._emscripten_bind_btFaceArray___destroy___0=function(){return(Zl=b._emscripten_bind_btFaceArray___destroy___0=b.asm.Yj).apply(null,arguments)},$l=b._emscripten_bind_btConvexPolyhedron_get_m_vertices_0=function(){return($l=b._emscripten_bind_btConvexPolyhedron_get_m_vertices_0=b.asm.Zj).apply(null,arguments)},am=b._emscripten_bind_btConvexPolyhedron_set_m_vertices_1=function(){return(am=b._emscripten_bind_btConvexPolyhedron_set_m_vertices_1= +b.asm._j).apply(null,arguments)},bm=b._emscripten_bind_btConvexPolyhedron_get_m_faces_0=function(){return(bm=b._emscripten_bind_btConvexPolyhedron_get_m_faces_0=b.asm.$j).apply(null,arguments)},cm=b._emscripten_bind_btConvexPolyhedron_set_m_faces_1=function(){return(cm=b._emscripten_bind_btConvexPolyhedron_set_m_faces_1=b.asm.ak).apply(null,arguments)},dm=b._emscripten_bind_btConvexPolyhedron___destroy___0=function(){return(dm=b._emscripten_bind_btConvexPolyhedron___destroy___0=b.asm.bk).apply(null, +arguments)},em=b._emscripten_bind_btConvexHullShape_btConvexHullShape_0=function(){return(em=b._emscripten_bind_btConvexHullShape_btConvexHullShape_0=b.asm.ck).apply(null,arguments)},fm=b._emscripten_bind_btConvexHullShape_btConvexHullShape_1=function(){return(fm=b._emscripten_bind_btConvexHullShape_btConvexHullShape_1=b.asm.dk).apply(null,arguments)},gm=b._emscripten_bind_btConvexHullShape_btConvexHullShape_2=function(){return(gm=b._emscripten_bind_btConvexHullShape_btConvexHullShape_2=b.asm.ek).apply(null, +arguments)},hm=b._emscripten_bind_btConvexHullShape_addPoint_1=function(){return(hm=b._emscripten_bind_btConvexHullShape_addPoint_1=b.asm.fk).apply(null,arguments)},im=b._emscripten_bind_btConvexHullShape_addPoint_2=function(){return(im=b._emscripten_bind_btConvexHullShape_addPoint_2=b.asm.gk).apply(null,arguments)},jm=b._emscripten_bind_btConvexHullShape_setMargin_1=function(){return(jm=b._emscripten_bind_btConvexHullShape_setMargin_1=b.asm.hk).apply(null,arguments)},km=b._emscripten_bind_btConvexHullShape_getMargin_0= +function(){return(km=b._emscripten_bind_btConvexHullShape_getMargin_0=b.asm.ik).apply(null,arguments)},lm=b._emscripten_bind_btConvexHullShape_getNumVertices_0=function(){return(lm=b._emscripten_bind_btConvexHullShape_getNumVertices_0=b.asm.jk).apply(null,arguments)},mm=b._emscripten_bind_btConvexHullShape_initializePolyhedralFeatures_1=function(){return(mm=b._emscripten_bind_btConvexHullShape_initializePolyhedralFeatures_1=b.asm.kk).apply(null,arguments)},nm=b._emscripten_bind_btConvexHullShape_recalcLocalAabb_0= +function(){return(nm=b._emscripten_bind_btConvexHullShape_recalcLocalAabb_0=b.asm.lk).apply(null,arguments)},om=b._emscripten_bind_btConvexHullShape_getConvexPolyhedron_0=function(){return(om=b._emscripten_bind_btConvexHullShape_getConvexPolyhedron_0=b.asm.mk).apply(null,arguments)},pm=b._emscripten_bind_btConvexHullShape_setLocalScaling_1=function(){return(pm=b._emscripten_bind_btConvexHullShape_setLocalScaling_1=b.asm.nk).apply(null,arguments)},qm=b._emscripten_bind_btConvexHullShape_getLocalScaling_0= +function(){return(qm=b._emscripten_bind_btConvexHullShape_getLocalScaling_0=b.asm.ok).apply(null,arguments)},rm=b._emscripten_bind_btConvexHullShape_calculateLocalInertia_2=function(){return(rm=b._emscripten_bind_btConvexHullShape_calculateLocalInertia_2=b.asm.pk).apply(null,arguments)},sm=b._emscripten_bind_btConvexHullShape___destroy___0=function(){return(sm=b._emscripten_bind_btConvexHullShape___destroy___0=b.asm.qk).apply(null,arguments)},tm=b._emscripten_bind_btShapeHull_btShapeHull_1=function(){return(tm= +b._emscripten_bind_btShapeHull_btShapeHull_1=b.asm.rk).apply(null,arguments)},um=b._emscripten_bind_btShapeHull_buildHull_1=function(){return(um=b._emscripten_bind_btShapeHull_buildHull_1=b.asm.sk).apply(null,arguments)},wm=b._emscripten_bind_btShapeHull_numVertices_0=function(){return(wm=b._emscripten_bind_btShapeHull_numVertices_0=b.asm.tk).apply(null,arguments)},xm=b._emscripten_bind_btShapeHull_getVertexPointer_0=function(){return(xm=b._emscripten_bind_btShapeHull_getVertexPointer_0=b.asm.uk).apply(null, +arguments)},ym=b._emscripten_bind_btShapeHull___destroy___0=function(){return(ym=b._emscripten_bind_btShapeHull___destroy___0=b.asm.vk).apply(null,arguments)},zm=b._emscripten_bind_btCompoundShape_btCompoundShape_0=function(){return(zm=b._emscripten_bind_btCompoundShape_btCompoundShape_0=b.asm.wk).apply(null,arguments)},Am=b._emscripten_bind_btCompoundShape_btCompoundShape_1=function(){return(Am=b._emscripten_bind_btCompoundShape_btCompoundShape_1=b.asm.xk).apply(null,arguments)},Bm=b._emscripten_bind_btCompoundShape_addChildShape_2= +function(){return(Bm=b._emscripten_bind_btCompoundShape_addChildShape_2=b.asm.yk).apply(null,arguments)},Cm=b._emscripten_bind_btCompoundShape_removeChildShape_1=function(){return(Cm=b._emscripten_bind_btCompoundShape_removeChildShape_1=b.asm.zk).apply(null,arguments)},Dm=b._emscripten_bind_btCompoundShape_removeChildShapeByIndex_1=function(){return(Dm=b._emscripten_bind_btCompoundShape_removeChildShapeByIndex_1=b.asm.Ak).apply(null,arguments)},Em=b._emscripten_bind_btCompoundShape_getNumChildShapes_0= +function(){return(Em=b._emscripten_bind_btCompoundShape_getNumChildShapes_0=b.asm.Bk).apply(null,arguments)},Fm=b._emscripten_bind_btCompoundShape_getChildShape_1=function(){return(Fm=b._emscripten_bind_btCompoundShape_getChildShape_1=b.asm.Ck).apply(null,arguments)},Gm=b._emscripten_bind_btCompoundShape_updateChildTransform_2=function(){return(Gm=b._emscripten_bind_btCompoundShape_updateChildTransform_2=b.asm.Dk).apply(null,arguments)},Hm=b._emscripten_bind_btCompoundShape_updateChildTransform_3= +function(){return(Hm=b._emscripten_bind_btCompoundShape_updateChildTransform_3=b.asm.Ek).apply(null,arguments)},Im=b._emscripten_bind_btCompoundShape_setMargin_1=function(){return(Im=b._emscripten_bind_btCompoundShape_setMargin_1=b.asm.Fk).apply(null,arguments)},Jm=b._emscripten_bind_btCompoundShape_getMargin_0=function(){return(Jm=b._emscripten_bind_btCompoundShape_getMargin_0=b.asm.Gk).apply(null,arguments)},Km=b._emscripten_bind_btCompoundShape_setLocalScaling_1=function(){return(Km=b._emscripten_bind_btCompoundShape_setLocalScaling_1= +b.asm.Hk).apply(null,arguments)},Lm=b._emscripten_bind_btCompoundShape_getLocalScaling_0=function(){return(Lm=b._emscripten_bind_btCompoundShape_getLocalScaling_0=b.asm.Ik).apply(null,arguments)},Mm=b._emscripten_bind_btCompoundShape_calculateLocalInertia_2=function(){return(Mm=b._emscripten_bind_btCompoundShape_calculateLocalInertia_2=b.asm.Jk).apply(null,arguments)},Nm=b._emscripten_bind_btCompoundShape___destroy___0=function(){return(Nm=b._emscripten_bind_btCompoundShape___destroy___0=b.asm.Kk).apply(null, +arguments)},Om=b._emscripten_bind_btIndexedMesh_get_m_numTriangles_0=function(){return(Om=b._emscripten_bind_btIndexedMesh_get_m_numTriangles_0=b.asm.Lk).apply(null,arguments)},Pm=b._emscripten_bind_btIndexedMesh_set_m_numTriangles_1=function(){return(Pm=b._emscripten_bind_btIndexedMesh_set_m_numTriangles_1=b.asm.Mk).apply(null,arguments)},Qm=b._emscripten_bind_btIndexedMesh___destroy___0=function(){return(Qm=b._emscripten_bind_btIndexedMesh___destroy___0=b.asm.Nk).apply(null,arguments)},Rm=b._emscripten_bind_btIndexedMeshArray_size_0= +function(){return(Rm=b._emscripten_bind_btIndexedMeshArray_size_0=b.asm.Ok).apply(null,arguments)},Sm=b._emscripten_bind_btIndexedMeshArray_at_1=function(){return(Sm=b._emscripten_bind_btIndexedMeshArray_at_1=b.asm.Pk).apply(null,arguments)},Tm=b._emscripten_bind_btIndexedMeshArray___destroy___0=function(){return(Tm=b._emscripten_bind_btIndexedMeshArray___destroy___0=b.asm.Qk).apply(null,arguments)},Um=b._emscripten_bind_btTriangleMesh_btTriangleMesh_0=function(){return(Um=b._emscripten_bind_btTriangleMesh_btTriangleMesh_0= +b.asm.Rk).apply(null,arguments)},Vm=b._emscripten_bind_btTriangleMesh_btTriangleMesh_1=function(){return(Vm=b._emscripten_bind_btTriangleMesh_btTriangleMesh_1=b.asm.Sk).apply(null,arguments)},Wm=b._emscripten_bind_btTriangleMesh_btTriangleMesh_2=function(){return(Wm=b._emscripten_bind_btTriangleMesh_btTriangleMesh_2=b.asm.Tk).apply(null,arguments)},Xm=b._emscripten_bind_btTriangleMesh_addTriangle_3=function(){return(Xm=b._emscripten_bind_btTriangleMesh_addTriangle_3=b.asm.Uk).apply(null,arguments)}, +Ym=b._emscripten_bind_btTriangleMesh_addTriangle_4=function(){return(Ym=b._emscripten_bind_btTriangleMesh_addTriangle_4=b.asm.Vk).apply(null,arguments)},Zm=b._emscripten_bind_btTriangleMesh_findOrAddVertex_2=function(){return(Zm=b._emscripten_bind_btTriangleMesh_findOrAddVertex_2=b.asm.Wk).apply(null,arguments)},$m=b._emscripten_bind_btTriangleMesh_addIndex_1=function(){return($m=b._emscripten_bind_btTriangleMesh_addIndex_1=b.asm.Xk).apply(null,arguments)},an=b._emscripten_bind_btTriangleMesh_getIndexedMeshArray_0= +function(){return(an=b._emscripten_bind_btTriangleMesh_getIndexedMeshArray_0=b.asm.Yk).apply(null,arguments)},bn=b._emscripten_bind_btTriangleMesh_setScaling_1=function(){return(bn=b._emscripten_bind_btTriangleMesh_setScaling_1=b.asm.Zk).apply(null,arguments)},cn=b._emscripten_bind_btTriangleMesh___destroy___0=function(){return(cn=b._emscripten_bind_btTriangleMesh___destroy___0=b.asm._k).apply(null,arguments)},dn=b._emscripten_bind_btEmptyShape_btEmptyShape_0=function(){return(dn=b._emscripten_bind_btEmptyShape_btEmptyShape_0= +b.asm.$k).apply(null,arguments)},en=b._emscripten_bind_btEmptyShape_setLocalScaling_1=function(){return(en=b._emscripten_bind_btEmptyShape_setLocalScaling_1=b.asm.al).apply(null,arguments)},fn=b._emscripten_bind_btEmptyShape_getLocalScaling_0=function(){return(fn=b._emscripten_bind_btEmptyShape_getLocalScaling_0=b.asm.bl).apply(null,arguments)},gn=b._emscripten_bind_btEmptyShape_calculateLocalInertia_2=function(){return(gn=b._emscripten_bind_btEmptyShape_calculateLocalInertia_2=b.asm.cl).apply(null, +arguments)},hn=b._emscripten_bind_btEmptyShape___destroy___0=function(){return(hn=b._emscripten_bind_btEmptyShape___destroy___0=b.asm.dl).apply(null,arguments)},jn=b._emscripten_bind_btStaticPlaneShape_btStaticPlaneShape_2=function(){return(jn=b._emscripten_bind_btStaticPlaneShape_btStaticPlaneShape_2=b.asm.el).apply(null,arguments)},kn=b._emscripten_bind_btStaticPlaneShape_setLocalScaling_1=function(){return(kn=b._emscripten_bind_btStaticPlaneShape_setLocalScaling_1=b.asm.fl).apply(null,arguments)}, +ln=b._emscripten_bind_btStaticPlaneShape_getLocalScaling_0=function(){return(ln=b._emscripten_bind_btStaticPlaneShape_getLocalScaling_0=b.asm.gl).apply(null,arguments)},mn=b._emscripten_bind_btStaticPlaneShape_calculateLocalInertia_2=function(){return(mn=b._emscripten_bind_btStaticPlaneShape_calculateLocalInertia_2=b.asm.hl).apply(null,arguments)},nn=b._emscripten_bind_btStaticPlaneShape___destroy___0=function(){return(nn=b._emscripten_bind_btStaticPlaneShape___destroy___0=b.asm.il).apply(null,arguments)}, +on=b._emscripten_bind_btBvhTriangleMeshShape_btBvhTriangleMeshShape_2=function(){return(on=b._emscripten_bind_btBvhTriangleMeshShape_btBvhTriangleMeshShape_2=b.asm.jl).apply(null,arguments)},pn=b._emscripten_bind_btBvhTriangleMeshShape_btBvhTriangleMeshShape_3=function(){return(pn=b._emscripten_bind_btBvhTriangleMeshShape_btBvhTriangleMeshShape_3=b.asm.kl).apply(null,arguments)},qn=b._emscripten_bind_btBvhTriangleMeshShape_setLocalScaling_1=function(){return(qn=b._emscripten_bind_btBvhTriangleMeshShape_setLocalScaling_1= +b.asm.ll).apply(null,arguments)},rn=b._emscripten_bind_btBvhTriangleMeshShape_getLocalScaling_0=function(){return(rn=b._emscripten_bind_btBvhTriangleMeshShape_getLocalScaling_0=b.asm.ml).apply(null,arguments)},sn=b._emscripten_bind_btBvhTriangleMeshShape_calculateLocalInertia_2=function(){return(sn=b._emscripten_bind_btBvhTriangleMeshShape_calculateLocalInertia_2=b.asm.nl).apply(null,arguments)},tn=b._emscripten_bind_btBvhTriangleMeshShape___destroy___0=function(){return(tn=b._emscripten_bind_btBvhTriangleMeshShape___destroy___0= +b.asm.ol).apply(null,arguments)},un=b._emscripten_bind_btHeightfieldTerrainShape_btHeightfieldTerrainShape_9=function(){return(un=b._emscripten_bind_btHeightfieldTerrainShape_btHeightfieldTerrainShape_9=b.asm.pl).apply(null,arguments)},vn=b._emscripten_bind_btHeightfieldTerrainShape_setMargin_1=function(){return(vn=b._emscripten_bind_btHeightfieldTerrainShape_setMargin_1=b.asm.ql).apply(null,arguments)},wn=b._emscripten_bind_btHeightfieldTerrainShape_getMargin_0=function(){return(wn=b._emscripten_bind_btHeightfieldTerrainShape_getMargin_0= +b.asm.rl).apply(null,arguments)},xn=b._emscripten_bind_btHeightfieldTerrainShape_setLocalScaling_1=function(){return(xn=b._emscripten_bind_btHeightfieldTerrainShape_setLocalScaling_1=b.asm.sl).apply(null,arguments)},yn=b._emscripten_bind_btHeightfieldTerrainShape_getLocalScaling_0=function(){return(yn=b._emscripten_bind_btHeightfieldTerrainShape_getLocalScaling_0=b.asm.tl).apply(null,arguments)},zn=b._emscripten_bind_btHeightfieldTerrainShape_calculateLocalInertia_2=function(){return(zn=b._emscripten_bind_btHeightfieldTerrainShape_calculateLocalInertia_2= +b.asm.ul).apply(null,arguments)},An=b._emscripten_bind_btHeightfieldTerrainShape___destroy___0=function(){return(An=b._emscripten_bind_btHeightfieldTerrainShape___destroy___0=b.asm.vl).apply(null,arguments)},Bn=b._emscripten_bind_btAABB_btAABB_4=function(){return(Bn=b._emscripten_bind_btAABB_btAABB_4=b.asm.wl).apply(null,arguments)},Cn=b._emscripten_bind_btAABB_invalidate_0=function(){return(Cn=b._emscripten_bind_btAABB_invalidate_0=b.asm.xl).apply(null,arguments)},Dn=b._emscripten_bind_btAABB_increment_margin_1= +function(){return(Dn=b._emscripten_bind_btAABB_increment_margin_1=b.asm.yl).apply(null,arguments)},En=b._emscripten_bind_btAABB_copy_with_margin_2=function(){return(En=b._emscripten_bind_btAABB_copy_with_margin_2=b.asm.zl).apply(null,arguments)},Fn=b._emscripten_bind_btAABB___destroy___0=function(){return(Fn=b._emscripten_bind_btAABB___destroy___0=b.asm.Al).apply(null,arguments)},Gn=b._emscripten_bind_btPrimitiveTriangle_btPrimitiveTriangle_0=function(){return(Gn=b._emscripten_bind_btPrimitiveTriangle_btPrimitiveTriangle_0= +b.asm.Bl).apply(null,arguments)},Hn=b._emscripten_bind_btPrimitiveTriangle___destroy___0=function(){return(Hn=b._emscripten_bind_btPrimitiveTriangle___destroy___0=b.asm.Cl).apply(null,arguments)},In=b._emscripten_bind_btTriangleShapeEx_btTriangleShapeEx_3=function(){return(In=b._emscripten_bind_btTriangleShapeEx_btTriangleShapeEx_3=b.asm.Dl).apply(null,arguments)},Jn=b._emscripten_bind_btTriangleShapeEx_getAabb_3=function(){return(Jn=b._emscripten_bind_btTriangleShapeEx_getAabb_3=b.asm.El).apply(null, +arguments)},Kn=b._emscripten_bind_btTriangleShapeEx_applyTransform_1=function(){return(Kn=b._emscripten_bind_btTriangleShapeEx_applyTransform_1=b.asm.Fl).apply(null,arguments)},Ln=b._emscripten_bind_btTriangleShapeEx_buildTriPlane_1=function(){return(Ln=b._emscripten_bind_btTriangleShapeEx_buildTriPlane_1=b.asm.Gl).apply(null,arguments)},Mn=b._emscripten_bind_btTriangleShapeEx___destroy___0=function(){return(Mn=b._emscripten_bind_btTriangleShapeEx___destroy___0=b.asm.Hl).apply(null,arguments)},Nn= +b._emscripten_bind_btTetrahedronShapeEx_btTetrahedronShapeEx_0=function(){return(Nn=b._emscripten_bind_btTetrahedronShapeEx_btTetrahedronShapeEx_0=b.asm.Il).apply(null,arguments)},On=b._emscripten_bind_btTetrahedronShapeEx_setVertices_4=function(){return(On=b._emscripten_bind_btTetrahedronShapeEx_setVertices_4=b.asm.Jl).apply(null,arguments)},Pn=b._emscripten_bind_btTetrahedronShapeEx___destroy___0=function(){return(Pn=b._emscripten_bind_btTetrahedronShapeEx___destroy___0=b.asm.Kl).apply(null,arguments)}, +Qn=b._emscripten_bind_CompoundPrimitiveManager_get_primitive_count_0=function(){return(Qn=b._emscripten_bind_CompoundPrimitiveManager_get_primitive_count_0=b.asm.Ll).apply(null,arguments)},Rn=b._emscripten_bind_CompoundPrimitiveManager_get_primitive_box_2=function(){return(Rn=b._emscripten_bind_CompoundPrimitiveManager_get_primitive_box_2=b.asm.Ml).apply(null,arguments)},Sn=b._emscripten_bind_CompoundPrimitiveManager_get_primitive_triangle_2=function(){return(Sn=b._emscripten_bind_CompoundPrimitiveManager_get_primitive_triangle_2= +b.asm.Nl).apply(null,arguments)},Tn=b._emscripten_bind_CompoundPrimitiveManager_is_trimesh_0=function(){return(Tn=b._emscripten_bind_CompoundPrimitiveManager_is_trimesh_0=b.asm.Ol).apply(null,arguments)},Un=b._emscripten_bind_CompoundPrimitiveManager_get_m_compoundShape_0=function(){return(Un=b._emscripten_bind_CompoundPrimitiveManager_get_m_compoundShape_0=b.asm.Pl).apply(null,arguments)},Vn=b._emscripten_bind_CompoundPrimitiveManager_set_m_compoundShape_1=function(){return(Vn=b._emscripten_bind_CompoundPrimitiveManager_set_m_compoundShape_1= +b.asm.Ql).apply(null,arguments)},Wn=b._emscripten_bind_CompoundPrimitiveManager___destroy___0=function(){return(Wn=b._emscripten_bind_CompoundPrimitiveManager___destroy___0=b.asm.Rl).apply(null,arguments)},Xn=b._emscripten_bind_btGImpactCompoundShape_btGImpactCompoundShape_0=function(){return(Xn=b._emscripten_bind_btGImpactCompoundShape_btGImpactCompoundShape_0=b.asm.Sl).apply(null,arguments)},Yn=b._emscripten_bind_btGImpactCompoundShape_btGImpactCompoundShape_1=function(){return(Yn=b._emscripten_bind_btGImpactCompoundShape_btGImpactCompoundShape_1= +b.asm.Tl).apply(null,arguments)},Zn=b._emscripten_bind_btGImpactCompoundShape_childrenHasTransform_0=function(){return(Zn=b._emscripten_bind_btGImpactCompoundShape_childrenHasTransform_0=b.asm.Ul).apply(null,arguments)},$n=b._emscripten_bind_btGImpactCompoundShape_getPrimitiveManager_0=function(){return($n=b._emscripten_bind_btGImpactCompoundShape_getPrimitiveManager_0=b.asm.Vl).apply(null,arguments)},ao=b._emscripten_bind_btGImpactCompoundShape_getCompoundPrimitiveManager_0=function(){return(ao= +b._emscripten_bind_btGImpactCompoundShape_getCompoundPrimitiveManager_0=b.asm.Wl).apply(null,arguments)},bo=b._emscripten_bind_btGImpactCompoundShape_getNumChildShapes_0=function(){return(bo=b._emscripten_bind_btGImpactCompoundShape_getNumChildShapes_0=b.asm.Xl).apply(null,arguments)},co=b._emscripten_bind_btGImpactCompoundShape_addChildShape_2=function(){return(co=b._emscripten_bind_btGImpactCompoundShape_addChildShape_2=b.asm.Yl).apply(null,arguments)},eo=b._emscripten_bind_btGImpactCompoundShape_getChildShape_1= +function(){return(eo=b._emscripten_bind_btGImpactCompoundShape_getChildShape_1=b.asm.Zl).apply(null,arguments)},fo=b._emscripten_bind_btGImpactCompoundShape_getChildAabb_4=function(){return(fo=b._emscripten_bind_btGImpactCompoundShape_getChildAabb_4=b.asm._l).apply(null,arguments)},go=b._emscripten_bind_btGImpactCompoundShape_getChildTransform_1=function(){return(go=b._emscripten_bind_btGImpactCompoundShape_getChildTransform_1=b.asm.$l).apply(null,arguments)},ho=b._emscripten_bind_btGImpactCompoundShape_setChildTransform_2= +function(){return(ho=b._emscripten_bind_btGImpactCompoundShape_setChildTransform_2=b.asm.am).apply(null,arguments)},io=b._emscripten_bind_btGImpactCompoundShape_calculateLocalInertia_2=function(){return(io=b._emscripten_bind_btGImpactCompoundShape_calculateLocalInertia_2=b.asm.bm).apply(null,arguments)},jo=b._emscripten_bind_btGImpactCompoundShape_getName_0=function(){return(jo=b._emscripten_bind_btGImpactCompoundShape_getName_0=b.asm.cm).apply(null,arguments)},ko=b._emscripten_bind_btGImpactCompoundShape_getGImpactShapeType_0= +function(){return(ko=b._emscripten_bind_btGImpactCompoundShape_getGImpactShapeType_0=b.asm.dm).apply(null,arguments)},lo=b._emscripten_bind_btGImpactCompoundShape_setLocalScaling_1=function(){return(lo=b._emscripten_bind_btGImpactCompoundShape_setLocalScaling_1=b.asm.em).apply(null,arguments)},mo=b._emscripten_bind_btGImpactCompoundShape_getLocalScaling_0=function(){return(mo=b._emscripten_bind_btGImpactCompoundShape_getLocalScaling_0=b.asm.fm).apply(null,arguments)},no=b._emscripten_bind_btGImpactCompoundShape_updateBound_0= +function(){return(no=b._emscripten_bind_btGImpactCompoundShape_updateBound_0=b.asm.gm).apply(null,arguments)},oo=b._emscripten_bind_btGImpactCompoundShape_postUpdate_0=function(){return(oo=b._emscripten_bind_btGImpactCompoundShape_postUpdate_0=b.asm.hm).apply(null,arguments)},po=b._emscripten_bind_btGImpactCompoundShape_getShapeType_0=function(){return(po=b._emscripten_bind_btGImpactCompoundShape_getShapeType_0=b.asm.im).apply(null,arguments)},qo=b._emscripten_bind_btGImpactCompoundShape_needsRetrieveTriangles_0= +function(){return(qo=b._emscripten_bind_btGImpactCompoundShape_needsRetrieveTriangles_0=b.asm.jm).apply(null,arguments)},ro=b._emscripten_bind_btGImpactCompoundShape_needsRetrieveTetrahedrons_0=function(){return(ro=b._emscripten_bind_btGImpactCompoundShape_needsRetrieveTetrahedrons_0=b.asm.km).apply(null,arguments)},so=b._emscripten_bind_btGImpactCompoundShape_getBulletTriangle_2=function(){return(so=b._emscripten_bind_btGImpactCompoundShape_getBulletTriangle_2=b.asm.lm).apply(null,arguments)},to= +b._emscripten_bind_btGImpactCompoundShape_getBulletTetrahedron_2=function(){return(to=b._emscripten_bind_btGImpactCompoundShape_getBulletTetrahedron_2=b.asm.mm).apply(null,arguments)},uo=b._emscripten_bind_btGImpactCompoundShape___destroy___0=function(){return(uo=b._emscripten_bind_btGImpactCompoundShape___destroy___0=b.asm.nm).apply(null,arguments)},vo=b._emscripten_bind_TrimeshPrimitiveManager_TrimeshPrimitiveManager_0=function(){return(vo=b._emscripten_bind_TrimeshPrimitiveManager_TrimeshPrimitiveManager_0= +b.asm.om).apply(null,arguments)},wo=b._emscripten_bind_TrimeshPrimitiveManager_TrimeshPrimitiveManager_1=function(){return(wo=b._emscripten_bind_TrimeshPrimitiveManager_TrimeshPrimitiveManager_1=b.asm.pm).apply(null,arguments)},xo=b._emscripten_bind_TrimeshPrimitiveManager_lock_0=function(){return(xo=b._emscripten_bind_TrimeshPrimitiveManager_lock_0=b.asm.qm).apply(null,arguments)},yo=b._emscripten_bind_TrimeshPrimitiveManager_unlock_0=function(){return(yo=b._emscripten_bind_TrimeshPrimitiveManager_unlock_0= +b.asm.rm).apply(null,arguments)},zo=b._emscripten_bind_TrimeshPrimitiveManager_is_trimesh_0=function(){return(zo=b._emscripten_bind_TrimeshPrimitiveManager_is_trimesh_0=b.asm.sm).apply(null,arguments)},Ao=b._emscripten_bind_TrimeshPrimitiveManager_get_vertex_count_0=function(){return(Ao=b._emscripten_bind_TrimeshPrimitiveManager_get_vertex_count_0=b.asm.tm).apply(null,arguments)},Bo=b._emscripten_bind_TrimeshPrimitiveManager_get_indices_4=function(){return(Bo=b._emscripten_bind_TrimeshPrimitiveManager_get_indices_4= +b.asm.um).apply(null,arguments)},Co=b._emscripten_bind_TrimeshPrimitiveManager_get_vertex_2=function(){return(Co=b._emscripten_bind_TrimeshPrimitiveManager_get_vertex_2=b.asm.vm).apply(null,arguments)},Do=b._emscripten_bind_TrimeshPrimitiveManager_get_bullet_triangle_2=function(){return(Do=b._emscripten_bind_TrimeshPrimitiveManager_get_bullet_triangle_2=b.asm.wm).apply(null,arguments)},Eo=b._emscripten_bind_TrimeshPrimitiveManager_get_m_margin_0=function(){return(Eo=b._emscripten_bind_TrimeshPrimitiveManager_get_m_margin_0= +b.asm.xm).apply(null,arguments)},Fo=b._emscripten_bind_TrimeshPrimitiveManager_set_m_margin_1=function(){return(Fo=b._emscripten_bind_TrimeshPrimitiveManager_set_m_margin_1=b.asm.ym).apply(null,arguments)},Go=b._emscripten_bind_TrimeshPrimitiveManager_get_m_meshInterface_0=function(){return(Go=b._emscripten_bind_TrimeshPrimitiveManager_get_m_meshInterface_0=b.asm.zm).apply(null,arguments)},Ho=b._emscripten_bind_TrimeshPrimitiveManager_set_m_meshInterface_1=function(){return(Ho=b._emscripten_bind_TrimeshPrimitiveManager_set_m_meshInterface_1= +b.asm.Am).apply(null,arguments)},Io=b._emscripten_bind_TrimeshPrimitiveManager_get_m_part_0=function(){return(Io=b._emscripten_bind_TrimeshPrimitiveManager_get_m_part_0=b.asm.Bm).apply(null,arguments)},Jo=b._emscripten_bind_TrimeshPrimitiveManager_set_m_part_1=function(){return(Jo=b._emscripten_bind_TrimeshPrimitiveManager_set_m_part_1=b.asm.Cm).apply(null,arguments)},Ko=b._emscripten_bind_TrimeshPrimitiveManager_get_m_lock_count_0=function(){return(Ko=b._emscripten_bind_TrimeshPrimitiveManager_get_m_lock_count_0= +b.asm.Dm).apply(null,arguments)},Lo=b._emscripten_bind_TrimeshPrimitiveManager_set_m_lock_count_1=function(){return(Lo=b._emscripten_bind_TrimeshPrimitiveManager_set_m_lock_count_1=b.asm.Em).apply(null,arguments)},Mo=b._emscripten_bind_TrimeshPrimitiveManager_get_numverts_0=function(){return(Mo=b._emscripten_bind_TrimeshPrimitiveManager_get_numverts_0=b.asm.Fm).apply(null,arguments)},No=b._emscripten_bind_TrimeshPrimitiveManager_set_numverts_1=function(){return(No=b._emscripten_bind_TrimeshPrimitiveManager_set_numverts_1= +b.asm.Gm).apply(null,arguments)},Oo=b._emscripten_bind_TrimeshPrimitiveManager_get_type_0=function(){return(Oo=b._emscripten_bind_TrimeshPrimitiveManager_get_type_0=b.asm.Hm).apply(null,arguments)},Po=b._emscripten_bind_TrimeshPrimitiveManager_set_type_1=function(){return(Po=b._emscripten_bind_TrimeshPrimitiveManager_set_type_1=b.asm.Im).apply(null,arguments)},Qo=b._emscripten_bind_TrimeshPrimitiveManager_get_stride_0=function(){return(Qo=b._emscripten_bind_TrimeshPrimitiveManager_get_stride_0=b.asm.Jm).apply(null, +arguments)},Ro=b._emscripten_bind_TrimeshPrimitiveManager_set_stride_1=function(){return(Ro=b._emscripten_bind_TrimeshPrimitiveManager_set_stride_1=b.asm.Km).apply(null,arguments)},So=b._emscripten_bind_TrimeshPrimitiveManager_get_indexstride_0=function(){return(So=b._emscripten_bind_TrimeshPrimitiveManager_get_indexstride_0=b.asm.Lm).apply(null,arguments)},To=b._emscripten_bind_TrimeshPrimitiveManager_set_indexstride_1=function(){return(To=b._emscripten_bind_TrimeshPrimitiveManager_set_indexstride_1= +b.asm.Mm).apply(null,arguments)},Uo=b._emscripten_bind_TrimeshPrimitiveManager_get_numfaces_0=function(){return(Uo=b._emscripten_bind_TrimeshPrimitiveManager_get_numfaces_0=b.asm.Nm).apply(null,arguments)},Vo=b._emscripten_bind_TrimeshPrimitiveManager_set_numfaces_1=function(){return(Vo=b._emscripten_bind_TrimeshPrimitiveManager_set_numfaces_1=b.asm.Om).apply(null,arguments)},Wo=b._emscripten_bind_TrimeshPrimitiveManager_get_indicestype_0=function(){return(Wo=b._emscripten_bind_TrimeshPrimitiveManager_get_indicestype_0= +b.asm.Pm).apply(null,arguments)},Xo=b._emscripten_bind_TrimeshPrimitiveManager_set_indicestype_1=function(){return(Xo=b._emscripten_bind_TrimeshPrimitiveManager_set_indicestype_1=b.asm.Qm).apply(null,arguments)},Yo=b._emscripten_bind_TrimeshPrimitiveManager___destroy___0=function(){return(Yo=b._emscripten_bind_TrimeshPrimitiveManager___destroy___0=b.asm.Rm).apply(null,arguments)},Zo=b._emscripten_bind_btGImpactMeshShapePart_btGImpactMeshShapePart_2=function(){return(Zo=b._emscripten_bind_btGImpactMeshShapePart_btGImpactMeshShapePart_2= +b.asm.Sm).apply(null,arguments)},$o=b._emscripten_bind_btGImpactMeshShapePart_getTrimeshPrimitiveManager_0=function(){return($o=b._emscripten_bind_btGImpactMeshShapePart_getTrimeshPrimitiveManager_0=b.asm.Tm).apply(null,arguments)},ap=b._emscripten_bind_btGImpactMeshShapePart_getVertexCount_0=function(){return(ap=b._emscripten_bind_btGImpactMeshShapePart_getVertexCount_0=b.asm.Um).apply(null,arguments)},bp=b._emscripten_bind_btGImpactMeshShapePart_getVertex_2=function(){return(bp=b._emscripten_bind_btGImpactMeshShapePart_getVertex_2= +b.asm.Vm).apply(null,arguments)},cp=b._emscripten_bind_btGImpactMeshShapePart_getPart_0=function(){return(cp=b._emscripten_bind_btGImpactMeshShapePart_getPart_0=b.asm.Wm).apply(null,arguments)},dp=b._emscripten_bind_btGImpactMeshShapePart_setLocalScaling_1=function(){return(dp=b._emscripten_bind_btGImpactMeshShapePart_setLocalScaling_1=b.asm.Xm).apply(null,arguments)},ep=b._emscripten_bind_btGImpactMeshShapePart_getLocalScaling_0=function(){return(ep=b._emscripten_bind_btGImpactMeshShapePart_getLocalScaling_0= +b.asm.Ym).apply(null,arguments)},fp=b._emscripten_bind_btGImpactMeshShapePart_updateBound_0=function(){return(fp=b._emscripten_bind_btGImpactMeshShapePart_updateBound_0=b.asm.Zm).apply(null,arguments)},gp=b._emscripten_bind_btGImpactMeshShapePart_postUpdate_0=function(){return(gp=b._emscripten_bind_btGImpactMeshShapePart_postUpdate_0=b.asm._m).apply(null,arguments)},hp=b._emscripten_bind_btGImpactMeshShapePart_getShapeType_0=function(){return(hp=b._emscripten_bind_btGImpactMeshShapePart_getShapeType_0= +b.asm.$m).apply(null,arguments)},ip=b._emscripten_bind_btGImpactMeshShapePart_needsRetrieveTriangles_0=function(){return(ip=b._emscripten_bind_btGImpactMeshShapePart_needsRetrieveTriangles_0=b.asm.an).apply(null,arguments)},jp=b._emscripten_bind_btGImpactMeshShapePart_needsRetrieveTetrahedrons_0=function(){return(jp=b._emscripten_bind_btGImpactMeshShapePart_needsRetrieveTetrahedrons_0=b.asm.bn).apply(null,arguments)},kp=b._emscripten_bind_btGImpactMeshShapePart_getBulletTriangle_2=function(){return(kp= +b._emscripten_bind_btGImpactMeshShapePart_getBulletTriangle_2=b.asm.cn).apply(null,arguments)},lp=b._emscripten_bind_btGImpactMeshShapePart_getBulletTetrahedron_2=function(){return(lp=b._emscripten_bind_btGImpactMeshShapePart_getBulletTetrahedron_2=b.asm.dn).apply(null,arguments)},mp=b._emscripten_bind_btGImpactMeshShapePart___destroy___0=function(){return(mp=b._emscripten_bind_btGImpactMeshShapePart___destroy___0=b.asm.en).apply(null,arguments)},np=b._emscripten_bind_btGImpactMeshShape_btGImpactMeshShape_1= +function(){return(np=b._emscripten_bind_btGImpactMeshShape_btGImpactMeshShape_1=b.asm.fn).apply(null,arguments)},op=b._emscripten_bind_btGImpactMeshShape_getMeshInterface_0=function(){return(op=b._emscripten_bind_btGImpactMeshShape_getMeshInterface_0=b.asm.gn).apply(null,arguments)},pp=b._emscripten_bind_btGImpactMeshShape_getMeshPartCount_0=function(){return(pp=b._emscripten_bind_btGImpactMeshShape_getMeshPartCount_0=b.asm.hn).apply(null,arguments)},qp=b._emscripten_bind_btGImpactMeshShape_getMeshPart_1= +function(){return(qp=b._emscripten_bind_btGImpactMeshShape_getMeshPart_1=b.asm.jn).apply(null,arguments)},rp=b._emscripten_bind_btGImpactMeshShape_calculateSerializeBufferSize_0=function(){return(rp=b._emscripten_bind_btGImpactMeshShape_calculateSerializeBufferSize_0=b.asm.kn).apply(null,arguments)},sp=b._emscripten_bind_btGImpactMeshShape_setLocalScaling_1=function(){return(sp=b._emscripten_bind_btGImpactMeshShape_setLocalScaling_1=b.asm.ln).apply(null,arguments)},tp=b._emscripten_bind_btGImpactMeshShape_getLocalScaling_0= +function(){return(tp=b._emscripten_bind_btGImpactMeshShape_getLocalScaling_0=b.asm.mn).apply(null,arguments)},up=b._emscripten_bind_btGImpactMeshShape_updateBound_0=function(){return(up=b._emscripten_bind_btGImpactMeshShape_updateBound_0=b.asm.nn).apply(null,arguments)},vp=b._emscripten_bind_btGImpactMeshShape_postUpdate_0=function(){return(vp=b._emscripten_bind_btGImpactMeshShape_postUpdate_0=b.asm.on).apply(null,arguments)},wp=b._emscripten_bind_btGImpactMeshShape_getShapeType_0=function(){return(wp= +b._emscripten_bind_btGImpactMeshShape_getShapeType_0=b.asm.pn).apply(null,arguments)},xp=b._emscripten_bind_btGImpactMeshShape_needsRetrieveTriangles_0=function(){return(xp=b._emscripten_bind_btGImpactMeshShape_needsRetrieveTriangles_0=b.asm.qn).apply(null,arguments)},yp=b._emscripten_bind_btGImpactMeshShape_needsRetrieveTetrahedrons_0=function(){return(yp=b._emscripten_bind_btGImpactMeshShape_needsRetrieveTetrahedrons_0=b.asm.rn).apply(null,arguments)},zp=b._emscripten_bind_btGImpactMeshShape_getBulletTriangle_2= +function(){return(zp=b._emscripten_bind_btGImpactMeshShape_getBulletTriangle_2=b.asm.sn).apply(null,arguments)},Ap=b._emscripten_bind_btGImpactMeshShape_getBulletTetrahedron_2=function(){return(Ap=b._emscripten_bind_btGImpactMeshShape_getBulletTetrahedron_2=b.asm.tn).apply(null,arguments)},Bp=b._emscripten_bind_btGImpactMeshShape___destroy___0=function(){return(Bp=b._emscripten_bind_btGImpactMeshShape___destroy___0=b.asm.un).apply(null,arguments)},Cp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_btCollisionAlgorithmConstructionInfo_0= +function(){return(Cp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_btCollisionAlgorithmConstructionInfo_0=b.asm.vn).apply(null,arguments)},Dp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_btCollisionAlgorithmConstructionInfo_2=function(){return(Dp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_btCollisionAlgorithmConstructionInfo_2=b.asm.wn).apply(null,arguments)},Ep=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_get_m_dispatcher1_0=function(){return(Ep=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_get_m_dispatcher1_0= +b.asm.xn).apply(null,arguments)},Fp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_set_m_dispatcher1_1=function(){return(Fp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_set_m_dispatcher1_1=b.asm.yn).apply(null,arguments)},Gp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_get_m_manifold_0=function(){return(Gp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_get_m_manifold_0=b.asm.zn).apply(null,arguments)},Hp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_set_m_manifold_1= +function(){return(Hp=b._emscripten_bind_btCollisionAlgorithmConstructionInfo_set_m_manifold_1=b.asm.An).apply(null,arguments)},Ip=b._emscripten_bind_btCollisionAlgorithmConstructionInfo___destroy___0=function(){return(Ip=b._emscripten_bind_btCollisionAlgorithmConstructionInfo___destroy___0=b.asm.Bn).apply(null,arguments)},Jp=b._emscripten_bind_btGImpactCollisionAlgorithm_btGImpactCollisionAlgorithm_3=function(){return(Jp=b._emscripten_bind_btGImpactCollisionAlgorithm_btGImpactCollisionAlgorithm_3= +b.asm.Cn).apply(null,arguments)},Kp=b._emscripten_bind_btGImpactCollisionAlgorithm_registerAlgorithm_1=function(){return(Kp=b._emscripten_bind_btGImpactCollisionAlgorithm_registerAlgorithm_1=b.asm.Dn).apply(null,arguments)},Lp=b._emscripten_bind_btGImpactCollisionAlgorithm___destroy___0=function(){return(Lp=b._emscripten_bind_btGImpactCollisionAlgorithm___destroy___0=b.asm.En).apply(null,arguments)},Mp=b._emscripten_bind_btDefaultCollisionConstructionInfo_btDefaultCollisionConstructionInfo_0=function(){return(Mp= +b._emscripten_bind_btDefaultCollisionConstructionInfo_btDefaultCollisionConstructionInfo_0=b.asm.Fn).apply(null,arguments)},Np=b._emscripten_bind_btDefaultCollisionConstructionInfo___destroy___0=function(){return(Np=b._emscripten_bind_btDefaultCollisionConstructionInfo___destroy___0=b.asm.Gn).apply(null,arguments)},Op=b._emscripten_bind_btPersistentManifold_btPersistentManifold_0=function(){return(Op=b._emscripten_bind_btPersistentManifold_btPersistentManifold_0=b.asm.Hn).apply(null,arguments)},Pp= +b._emscripten_bind_btPersistentManifold_getBody0_0=function(){return(Pp=b._emscripten_bind_btPersistentManifold_getBody0_0=b.asm.In).apply(null,arguments)},Qp=b._emscripten_bind_btPersistentManifold_getBody1_0=function(){return(Qp=b._emscripten_bind_btPersistentManifold_getBody1_0=b.asm.Jn).apply(null,arguments)},Rp=b._emscripten_bind_btPersistentManifold_getNumContacts_0=function(){return(Rp=b._emscripten_bind_btPersistentManifold_getNumContacts_0=b.asm.Kn).apply(null,arguments)},Sp=b._emscripten_bind_btPersistentManifold_getContactPoint_1= +function(){return(Sp=b._emscripten_bind_btPersistentManifold_getContactPoint_1=b.asm.Ln).apply(null,arguments)},Tp=b._emscripten_bind_btPersistentManifold___destroy___0=function(){return(Tp=b._emscripten_bind_btPersistentManifold___destroy___0=b.asm.Mn).apply(null,arguments)},Up=b._emscripten_bind_btCollisionDispatcher_btCollisionDispatcher_1=function(){return(Up=b._emscripten_bind_btCollisionDispatcher_btCollisionDispatcher_1=b.asm.Nn).apply(null,arguments)},Vp=b._emscripten_bind_btCollisionDispatcher_getNumManifolds_0= +function(){return(Vp=b._emscripten_bind_btCollisionDispatcher_getNumManifolds_0=b.asm.On).apply(null,arguments)},Wp=b._emscripten_bind_btCollisionDispatcher_getManifoldByIndexInternal_1=function(){return(Wp=b._emscripten_bind_btCollisionDispatcher_getManifoldByIndexInternal_1=b.asm.Pn).apply(null,arguments)},Xp=b._emscripten_bind_btCollisionDispatcher___destroy___0=function(){return(Xp=b._emscripten_bind_btCollisionDispatcher___destroy___0=b.asm.Qn).apply(null,arguments)},Yp=b._emscripten_bind_btOverlappingPairCallback___destroy___0= +function(){return(Yp=b._emscripten_bind_btOverlappingPairCallback___destroy___0=b.asm.Rn).apply(null,arguments)},Zp=b._emscripten_bind_btOverlappingPairCache_setInternalGhostPairCallback_1=function(){return(Zp=b._emscripten_bind_btOverlappingPairCache_setInternalGhostPairCallback_1=b.asm.Sn).apply(null,arguments)},$p=b._emscripten_bind_btOverlappingPairCache_getNumOverlappingPairs_0=function(){return($p=b._emscripten_bind_btOverlappingPairCache_getNumOverlappingPairs_0=b.asm.Tn).apply(null,arguments)}, +aq=b._emscripten_bind_btOverlappingPairCache___destroy___0=function(){return(aq=b._emscripten_bind_btOverlappingPairCache___destroy___0=b.asm.Un).apply(null,arguments)},bq=b._emscripten_bind_btAxisSweep3_btAxisSweep3_2=function(){return(bq=b._emscripten_bind_btAxisSweep3_btAxisSweep3_2=b.asm.Vn).apply(null,arguments)},cq=b._emscripten_bind_btAxisSweep3_btAxisSweep3_3=function(){return(cq=b._emscripten_bind_btAxisSweep3_btAxisSweep3_3=b.asm.Wn).apply(null,arguments)},dq=b._emscripten_bind_btAxisSweep3_btAxisSweep3_4= +function(){return(dq=b._emscripten_bind_btAxisSweep3_btAxisSweep3_4=b.asm.Xn).apply(null,arguments)},eq=b._emscripten_bind_btAxisSweep3_btAxisSweep3_5=function(){return(eq=b._emscripten_bind_btAxisSweep3_btAxisSweep3_5=b.asm.Yn).apply(null,arguments)},fq=b._emscripten_bind_btAxisSweep3___destroy___0=function(){return(fq=b._emscripten_bind_btAxisSweep3___destroy___0=b.asm.Zn).apply(null,arguments)},gq=b._emscripten_bind_btBroadphaseInterface_getOverlappingPairCache_0=function(){return(gq=b._emscripten_bind_btBroadphaseInterface_getOverlappingPairCache_0= +b.asm._n).apply(null,arguments)},hq=b._emscripten_bind_btBroadphaseInterface___destroy___0=function(){return(hq=b._emscripten_bind_btBroadphaseInterface___destroy___0=b.asm.$n).apply(null,arguments)},iq=b._emscripten_bind_btCollisionConfiguration___destroy___0=function(){return(iq=b._emscripten_bind_btCollisionConfiguration___destroy___0=b.asm.ao).apply(null,arguments)},jq=b._emscripten_bind_btDbvtBroadphase_btDbvtBroadphase_0=function(){return(jq=b._emscripten_bind_btDbvtBroadphase_btDbvtBroadphase_0= +b.asm.bo).apply(null,arguments)},kq=b._emscripten_bind_btDbvtBroadphase___destroy___0=function(){return(kq=b._emscripten_bind_btDbvtBroadphase___destroy___0=b.asm.co).apply(null,arguments)},lq=b._emscripten_bind_btBroadphaseProxy_get_m_collisionFilterGroup_0=function(){return(lq=b._emscripten_bind_btBroadphaseProxy_get_m_collisionFilterGroup_0=b.asm.eo).apply(null,arguments)},mq=b._emscripten_bind_btBroadphaseProxy_set_m_collisionFilterGroup_1=function(){return(mq=b._emscripten_bind_btBroadphaseProxy_set_m_collisionFilterGroup_1= +b.asm.fo).apply(null,arguments)},nq=b._emscripten_bind_btBroadphaseProxy_get_m_collisionFilterMask_0=function(){return(nq=b._emscripten_bind_btBroadphaseProxy_get_m_collisionFilterMask_0=b.asm.go).apply(null,arguments)},oq=b._emscripten_bind_btBroadphaseProxy_set_m_collisionFilterMask_1=function(){return(oq=b._emscripten_bind_btBroadphaseProxy_set_m_collisionFilterMask_1=b.asm.ho).apply(null,arguments)},pq=b._emscripten_bind_btBroadphaseProxy___destroy___0=function(){return(pq=b._emscripten_bind_btBroadphaseProxy___destroy___0= +b.asm.io).apply(null,arguments)},qq=b._emscripten_bind_btRigidBodyConstructionInfo_btRigidBodyConstructionInfo_3=function(){return(qq=b._emscripten_bind_btRigidBodyConstructionInfo_btRigidBodyConstructionInfo_3=b.asm.jo).apply(null,arguments)},rq=b._emscripten_bind_btRigidBodyConstructionInfo_btRigidBodyConstructionInfo_4=function(){return(rq=b._emscripten_bind_btRigidBodyConstructionInfo_btRigidBodyConstructionInfo_4=b.asm.ko).apply(null,arguments)},sq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_linearDamping_0= +function(){return(sq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_linearDamping_0=b.asm.lo).apply(null,arguments)},tq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_linearDamping_1=function(){return(tq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_linearDamping_1=b.asm.mo).apply(null,arguments)},uq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_angularDamping_0=function(){return(uq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_angularDamping_0=b.asm.no).apply(null, +arguments)},vq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_angularDamping_1=function(){return(vq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_angularDamping_1=b.asm.oo).apply(null,arguments)},wq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_friction_0=function(){return(wq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_friction_0=b.asm.po).apply(null,arguments)},xq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_friction_1=function(){return(xq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_friction_1= +b.asm.qo).apply(null,arguments)},yq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_rollingFriction_0=function(){return(yq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_rollingFriction_0=b.asm.ro).apply(null,arguments)},zq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_rollingFriction_1=function(){return(zq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_rollingFriction_1=b.asm.so).apply(null,arguments)},Aq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_restitution_0= +function(){return(Aq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_restitution_0=b.asm.to).apply(null,arguments)},Bq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_restitution_1=function(){return(Bq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_restitution_1=b.asm.uo).apply(null,arguments)},Cq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_linearSleepingThreshold_0=function(){return(Cq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_linearSleepingThreshold_0=b.asm.vo).apply(null, +arguments)},Dq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_linearSleepingThreshold_1=function(){return(Dq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_linearSleepingThreshold_1=b.asm.wo).apply(null,arguments)},Eq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_angularSleepingThreshold_0=function(){return(Eq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_angularSleepingThreshold_0=b.asm.xo).apply(null,arguments)},Fq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_angularSleepingThreshold_1= +function(){return(Fq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_angularSleepingThreshold_1=b.asm.yo).apply(null,arguments)},Gq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalDamping_0=function(){return(Gq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalDamping_0=b.asm.zo).apply(null,arguments)},Hq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalDamping_1=function(){return(Hq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalDamping_1= +b.asm.Ao).apply(null,arguments)},Iq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalDampingFactor_0=function(){return(Iq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalDampingFactor_0=b.asm.Bo).apply(null,arguments)},Jq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalDampingFactor_1=function(){return(Jq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalDampingFactor_1=b.asm.Co).apply(null,arguments)},Kq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalLinearDampingThresholdSqr_0= +function(){return(Kq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalLinearDampingThresholdSqr_0=b.asm.Do).apply(null,arguments)},Lq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalLinearDampingThresholdSqr_1=function(){return(Lq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalLinearDampingThresholdSqr_1=b.asm.Eo).apply(null,arguments)},Mq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalAngularDampingThresholdSqr_0=function(){return(Mq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalAngularDampingThresholdSqr_0= +b.asm.Fo).apply(null,arguments)},Nq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalAngularDampingThresholdSqr_1=function(){return(Nq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalAngularDampingThresholdSqr_1=b.asm.Go).apply(null,arguments)},Oq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalAngularDampingFactor_0=function(){return(Oq=b._emscripten_bind_btRigidBodyConstructionInfo_get_m_additionalAngularDampingFactor_0=b.asm.Ho).apply(null,arguments)}, +Pq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalAngularDampingFactor_1=function(){return(Pq=b._emscripten_bind_btRigidBodyConstructionInfo_set_m_additionalAngularDampingFactor_1=b.asm.Io).apply(null,arguments)},Qq=b._emscripten_bind_btRigidBodyConstructionInfo___destroy___0=function(){return(Qq=b._emscripten_bind_btRigidBodyConstructionInfo___destroy___0=b.asm.Jo).apply(null,arguments)},Rq=b._emscripten_bind_btRigidBody_btRigidBody_1=function(){return(Rq=b._emscripten_bind_btRigidBody_btRigidBody_1= +b.asm.Ko).apply(null,arguments)},Sq=b._emscripten_bind_btRigidBody_getCenterOfMassTransform_0=function(){return(Sq=b._emscripten_bind_btRigidBody_getCenterOfMassTransform_0=b.asm.Lo).apply(null,arguments)},Tq=b._emscripten_bind_btRigidBody_setCenterOfMassTransform_1=function(){return(Tq=b._emscripten_bind_btRigidBody_setCenterOfMassTransform_1=b.asm.Mo).apply(null,arguments)},Uq=b._emscripten_bind_btRigidBody_setSleepingThresholds_2=function(){return(Uq=b._emscripten_bind_btRigidBody_setSleepingThresholds_2= +b.asm.No).apply(null,arguments)},Vq=b._emscripten_bind_btRigidBody_getLinearDamping_0=function(){return(Vq=b._emscripten_bind_btRigidBody_getLinearDamping_0=b.asm.Oo).apply(null,arguments)},Wq=b._emscripten_bind_btRigidBody_getAngularDamping_0=function(){return(Wq=b._emscripten_bind_btRigidBody_getAngularDamping_0=b.asm.Po).apply(null,arguments)},Xq=b._emscripten_bind_btRigidBody_setDamping_2=function(){return(Xq=b._emscripten_bind_btRigidBody_setDamping_2=b.asm.Qo).apply(null,arguments)},Yq=b._emscripten_bind_btRigidBody_setMassProps_2= +function(){return(Yq=b._emscripten_bind_btRigidBody_setMassProps_2=b.asm.Ro).apply(null,arguments)},Zq=b._emscripten_bind_btRigidBody_getLinearFactor_0=function(){return(Zq=b._emscripten_bind_btRigidBody_getLinearFactor_0=b.asm.So).apply(null,arguments)},$q=b._emscripten_bind_btRigidBody_setLinearFactor_1=function(){return($q=b._emscripten_bind_btRigidBody_setLinearFactor_1=b.asm.To).apply(null,arguments)},ar=b._emscripten_bind_btRigidBody_applyTorque_1=function(){return(ar=b._emscripten_bind_btRigidBody_applyTorque_1= +b.asm.Uo).apply(null,arguments)},br=b._emscripten_bind_btRigidBody_applyLocalTorque_1=function(){return(br=b._emscripten_bind_btRigidBody_applyLocalTorque_1=b.asm.Vo).apply(null,arguments)},cr=b._emscripten_bind_btRigidBody_applyForce_2=function(){return(cr=b._emscripten_bind_btRigidBody_applyForce_2=b.asm.Wo).apply(null,arguments)},dr=b._emscripten_bind_btRigidBody_applyCentralForce_1=function(){return(dr=b._emscripten_bind_btRigidBody_applyCentralForce_1=b.asm.Xo).apply(null,arguments)},er=b._emscripten_bind_btRigidBody_applyCentralLocalForce_1= +function(){return(er=b._emscripten_bind_btRigidBody_applyCentralLocalForce_1=b.asm.Yo).apply(null,arguments)},fr=b._emscripten_bind_btRigidBody_applyTorqueImpulse_1=function(){return(fr=b._emscripten_bind_btRigidBody_applyTorqueImpulse_1=b.asm.Zo).apply(null,arguments)},gr=b._emscripten_bind_btRigidBody_applyImpulse_2=function(){return(gr=b._emscripten_bind_btRigidBody_applyImpulse_2=b.asm._o).apply(null,arguments)},hr=b._emscripten_bind_btRigidBody_applyCentralImpulse_1=function(){return(hr=b._emscripten_bind_btRigidBody_applyCentralImpulse_1= +b.asm.$o).apply(null,arguments)},ir=b._emscripten_bind_btRigidBody_updateInertiaTensor_0=function(){return(ir=b._emscripten_bind_btRigidBody_updateInertiaTensor_0=b.asm.ap).apply(null,arguments)},jr=b._emscripten_bind_btRigidBody_getLinearVelocity_0=function(){return(jr=b._emscripten_bind_btRigidBody_getLinearVelocity_0=b.asm.bp).apply(null,arguments)},kr=b._emscripten_bind_btRigidBody_getAngularVelocity_0=function(){return(kr=b._emscripten_bind_btRigidBody_getAngularVelocity_0=b.asm.cp).apply(null, +arguments)},lr=b._emscripten_bind_btRigidBody_setLinearVelocity_1=function(){return(lr=b._emscripten_bind_btRigidBody_setLinearVelocity_1=b.asm.dp).apply(null,arguments)},mr=b._emscripten_bind_btRigidBody_setAngularVelocity_1=function(){return(mr=b._emscripten_bind_btRigidBody_setAngularVelocity_1=b.asm.ep).apply(null,arguments)},nr=b._emscripten_bind_btRigidBody_getMotionState_0=function(){return(nr=b._emscripten_bind_btRigidBody_getMotionState_0=b.asm.fp).apply(null,arguments)},or=b._emscripten_bind_btRigidBody_setMotionState_1= +function(){return(or=b._emscripten_bind_btRigidBody_setMotionState_1=b.asm.gp).apply(null,arguments)},pr=b._emscripten_bind_btRigidBody_getAngularFactor_0=function(){return(pr=b._emscripten_bind_btRigidBody_getAngularFactor_0=b.asm.hp).apply(null,arguments)},qr=b._emscripten_bind_btRigidBody_setAngularFactor_1=function(){return(qr=b._emscripten_bind_btRigidBody_setAngularFactor_1=b.asm.ip).apply(null,arguments)},rr=b._emscripten_bind_btRigidBody_upcast_1=function(){return(rr=b._emscripten_bind_btRigidBody_upcast_1= +b.asm.jp).apply(null,arguments)},sr=b._emscripten_bind_btRigidBody_getAabb_2=function(){return(sr=b._emscripten_bind_btRigidBody_getAabb_2=b.asm.kp).apply(null,arguments)},tr=b._emscripten_bind_btRigidBody_applyGravity_0=function(){return(tr=b._emscripten_bind_btRigidBody_applyGravity_0=b.asm.lp).apply(null,arguments)},ur=b._emscripten_bind_btRigidBody_getGravity_0=function(){return(ur=b._emscripten_bind_btRigidBody_getGravity_0=b.asm.mp).apply(null,arguments)},vr=b._emscripten_bind_btRigidBody_setGravity_1= +function(){return(vr=b._emscripten_bind_btRigidBody_setGravity_1=b.asm.np).apply(null,arguments)},wr=b._emscripten_bind_btRigidBody_getBroadphaseProxy_0=function(){return(wr=b._emscripten_bind_btRigidBody_getBroadphaseProxy_0=b.asm.op).apply(null,arguments)},xr=b._emscripten_bind_btRigidBody_clearForces_0=function(){return(xr=b._emscripten_bind_btRigidBody_clearForces_0=b.asm.pp).apply(null,arguments)},yr=b._emscripten_bind_btRigidBody_setFlags_1=function(){return(yr=b._emscripten_bind_btRigidBody_setFlags_1= +b.asm.qp).apply(null,arguments)},zr=b._emscripten_bind_btRigidBody_getFlags_0=function(){return(zr=b._emscripten_bind_btRigidBody_getFlags_0=b.asm.rp).apply(null,arguments)},Ar=b._emscripten_bind_btRigidBody_setAnisotropicFriction_2=function(){return(Ar=b._emscripten_bind_btRigidBody_setAnisotropicFriction_2=b.asm.sp).apply(null,arguments)},Br=b._emscripten_bind_btRigidBody_getCollisionShape_0=function(){return(Br=b._emscripten_bind_btRigidBody_getCollisionShape_0=b.asm.tp).apply(null,arguments)}, +Cr=b._emscripten_bind_btRigidBody_setContactProcessingThreshold_1=function(){return(Cr=b._emscripten_bind_btRigidBody_setContactProcessingThreshold_1=b.asm.up).apply(null,arguments)},Dr=b._emscripten_bind_btRigidBody_setActivationState_1=function(){return(Dr=b._emscripten_bind_btRigidBody_setActivationState_1=b.asm.vp).apply(null,arguments)},Er=b._emscripten_bind_btRigidBody_forceActivationState_1=function(){return(Er=b._emscripten_bind_btRigidBody_forceActivationState_1=b.asm.wp).apply(null,arguments)}, +Fr=b._emscripten_bind_btRigidBody_activate_0=function(){return(Fr=b._emscripten_bind_btRigidBody_activate_0=b.asm.xp).apply(null,arguments)},Gr=b._emscripten_bind_btRigidBody_activate_1=function(){return(Gr=b._emscripten_bind_btRigidBody_activate_1=b.asm.yp).apply(null,arguments)},Hr=b._emscripten_bind_btRigidBody_isActive_0=function(){return(Hr=b._emscripten_bind_btRigidBody_isActive_0=b.asm.zp).apply(null,arguments)},Ir=b._emscripten_bind_btRigidBody_isKinematicObject_0=function(){return(Ir=b._emscripten_bind_btRigidBody_isKinematicObject_0= +b.asm.Ap).apply(null,arguments)},Jr=b._emscripten_bind_btRigidBody_isStaticObject_0=function(){return(Jr=b._emscripten_bind_btRigidBody_isStaticObject_0=b.asm.Bp).apply(null,arguments)},Kr=b._emscripten_bind_btRigidBody_isStaticOrKinematicObject_0=function(){return(Kr=b._emscripten_bind_btRigidBody_isStaticOrKinematicObject_0=b.asm.Cp).apply(null,arguments)},Lr=b._emscripten_bind_btRigidBody_getRestitution_0=function(){return(Lr=b._emscripten_bind_btRigidBody_getRestitution_0=b.asm.Dp).apply(null, +arguments)},Mr=b._emscripten_bind_btRigidBody_getFriction_0=function(){return(Mr=b._emscripten_bind_btRigidBody_getFriction_0=b.asm.Ep).apply(null,arguments)},Nr=b._emscripten_bind_btRigidBody_getRollingFriction_0=function(){return(Nr=b._emscripten_bind_btRigidBody_getRollingFriction_0=b.asm.Fp).apply(null,arguments)},Or=b._emscripten_bind_btRigidBody_setRestitution_1=function(){return(Or=b._emscripten_bind_btRigidBody_setRestitution_1=b.asm.Gp).apply(null,arguments)},Pr=b._emscripten_bind_btRigidBody_setFriction_1= +function(){return(Pr=b._emscripten_bind_btRigidBody_setFriction_1=b.asm.Hp).apply(null,arguments)},Qr=b._emscripten_bind_btRigidBody_setRollingFriction_1=function(){return(Qr=b._emscripten_bind_btRigidBody_setRollingFriction_1=b.asm.Ip).apply(null,arguments)},Rr=b._emscripten_bind_btRigidBody_getWorldTransform_0=function(){return(Rr=b._emscripten_bind_btRigidBody_getWorldTransform_0=b.asm.Jp).apply(null,arguments)},Sr=b._emscripten_bind_btRigidBody_getCollisionFlags_0=function(){return(Sr=b._emscripten_bind_btRigidBody_getCollisionFlags_0= +b.asm.Kp).apply(null,arguments)},Tr=b._emscripten_bind_btRigidBody_setCollisionFlags_1=function(){return(Tr=b._emscripten_bind_btRigidBody_setCollisionFlags_1=b.asm.Lp).apply(null,arguments)},Ur=b._emscripten_bind_btRigidBody_setWorldTransform_1=function(){return(Ur=b._emscripten_bind_btRigidBody_setWorldTransform_1=b.asm.Mp).apply(null,arguments)},Vr=b._emscripten_bind_btRigidBody_setCollisionShape_1=function(){return(Vr=b._emscripten_bind_btRigidBody_setCollisionShape_1=b.asm.Np).apply(null,arguments)}, +Wr=b._emscripten_bind_btRigidBody_setCcdMotionThreshold_1=function(){return(Wr=b._emscripten_bind_btRigidBody_setCcdMotionThreshold_1=b.asm.Op).apply(null,arguments)},Xr=b._emscripten_bind_btRigidBody_setCcdSweptSphereRadius_1=function(){return(Xr=b._emscripten_bind_btRigidBody_setCcdSweptSphereRadius_1=b.asm.Pp).apply(null,arguments)},Yr=b._emscripten_bind_btRigidBody_getUserIndex_0=function(){return(Yr=b._emscripten_bind_btRigidBody_getUserIndex_0=b.asm.Qp).apply(null,arguments)},Zr=b._emscripten_bind_btRigidBody_setUserIndex_1= +function(){return(Zr=b._emscripten_bind_btRigidBody_setUserIndex_1=b.asm.Rp).apply(null,arguments)},$r=b._emscripten_bind_btRigidBody_getUserPointer_0=function(){return($r=b._emscripten_bind_btRigidBody_getUserPointer_0=b.asm.Sp).apply(null,arguments)},as=b._emscripten_bind_btRigidBody_setUserPointer_1=function(){return(as=b._emscripten_bind_btRigidBody_setUserPointer_1=b.asm.Tp).apply(null,arguments)},bs=b._emscripten_bind_btRigidBody_getBroadphaseHandle_0=function(){return(bs=b._emscripten_bind_btRigidBody_getBroadphaseHandle_0= +b.asm.Up).apply(null,arguments)},cs=b._emscripten_bind_btRigidBody___destroy___0=function(){return(cs=b._emscripten_bind_btRigidBody___destroy___0=b.asm.Vp).apply(null,arguments)},ds=b._emscripten_bind_btConstraintSetting_btConstraintSetting_0=function(){return(ds=b._emscripten_bind_btConstraintSetting_btConstraintSetting_0=b.asm.Wp).apply(null,arguments)},es=b._emscripten_bind_btConstraintSetting_get_m_tau_0=function(){return(es=b._emscripten_bind_btConstraintSetting_get_m_tau_0=b.asm.Xp).apply(null, +arguments)},gs=b._emscripten_bind_btConstraintSetting_set_m_tau_1=function(){return(gs=b._emscripten_bind_btConstraintSetting_set_m_tau_1=b.asm.Yp).apply(null,arguments)},hs=b._emscripten_bind_btConstraintSetting_get_m_damping_0=function(){return(hs=b._emscripten_bind_btConstraintSetting_get_m_damping_0=b.asm.Zp).apply(null,arguments)},is=b._emscripten_bind_btConstraintSetting_set_m_damping_1=function(){return(is=b._emscripten_bind_btConstraintSetting_set_m_damping_1=b.asm._p).apply(null,arguments)}, +js=b._emscripten_bind_btConstraintSetting_get_m_impulseClamp_0=function(){return(js=b._emscripten_bind_btConstraintSetting_get_m_impulseClamp_0=b.asm.$p).apply(null,arguments)},ks=b._emscripten_bind_btConstraintSetting_set_m_impulseClamp_1=function(){return(ks=b._emscripten_bind_btConstraintSetting_set_m_impulseClamp_1=b.asm.aq).apply(null,arguments)},ls=b._emscripten_bind_btConstraintSetting___destroy___0=function(){return(ls=b._emscripten_bind_btConstraintSetting___destroy___0=b.asm.bq).apply(null, +arguments)},ms=b._emscripten_bind_btPoint2PointConstraint_btPoint2PointConstraint_2=function(){return(ms=b._emscripten_bind_btPoint2PointConstraint_btPoint2PointConstraint_2=b.asm.cq).apply(null,arguments)},ns=b._emscripten_bind_btPoint2PointConstraint_btPoint2PointConstraint_4=function(){return(ns=b._emscripten_bind_btPoint2PointConstraint_btPoint2PointConstraint_4=b.asm.dq).apply(null,arguments)},ps=b._emscripten_bind_btPoint2PointConstraint_setPivotA_1=function(){return(ps=b._emscripten_bind_btPoint2PointConstraint_setPivotA_1= +b.asm.eq).apply(null,arguments)},qs=b._emscripten_bind_btPoint2PointConstraint_setPivotB_1=function(){return(qs=b._emscripten_bind_btPoint2PointConstraint_setPivotB_1=b.asm.fq).apply(null,arguments)},rs=b._emscripten_bind_btPoint2PointConstraint_getPivotInA_0=function(){return(rs=b._emscripten_bind_btPoint2PointConstraint_getPivotInA_0=b.asm.gq).apply(null,arguments)},ss=b._emscripten_bind_btPoint2PointConstraint_getPivotInB_0=function(){return(ss=b._emscripten_bind_btPoint2PointConstraint_getPivotInB_0= +b.asm.hq).apply(null,arguments)},ts=b._emscripten_bind_btPoint2PointConstraint_enableFeedback_1=function(){return(ts=b._emscripten_bind_btPoint2PointConstraint_enableFeedback_1=b.asm.iq).apply(null,arguments)},us=b._emscripten_bind_btPoint2PointConstraint_getBreakingImpulseThreshold_0=function(){return(us=b._emscripten_bind_btPoint2PointConstraint_getBreakingImpulseThreshold_0=b.asm.jq).apply(null,arguments)},vs=b._emscripten_bind_btPoint2PointConstraint_setBreakingImpulseThreshold_1=function(){return(vs= +b._emscripten_bind_btPoint2PointConstraint_setBreakingImpulseThreshold_1=b.asm.kq).apply(null,arguments)},xs=b._emscripten_bind_btPoint2PointConstraint_getParam_2=function(){return(xs=b._emscripten_bind_btPoint2PointConstraint_getParam_2=b.asm.lq).apply(null,arguments)},ys=b._emscripten_bind_btPoint2PointConstraint_setParam_3=function(){return(ys=b._emscripten_bind_btPoint2PointConstraint_setParam_3=b.asm.mq).apply(null,arguments)},zs=b._emscripten_bind_btPoint2PointConstraint_get_m_setting_0=function(){return(zs= +b._emscripten_bind_btPoint2PointConstraint_get_m_setting_0=b.asm.nq).apply(null,arguments)},As=b._emscripten_bind_btPoint2PointConstraint_set_m_setting_1=function(){return(As=b._emscripten_bind_btPoint2PointConstraint_set_m_setting_1=b.asm.oq).apply(null,arguments)},Bs=b._emscripten_bind_btPoint2PointConstraint___destroy___0=function(){return(Bs=b._emscripten_bind_btPoint2PointConstraint___destroy___0=b.asm.pq).apply(null,arguments)},Cs=b._emscripten_bind_btGeneric6DofSpringConstraint_btGeneric6DofSpringConstraint_3= +function(){return(Cs=b._emscripten_bind_btGeneric6DofSpringConstraint_btGeneric6DofSpringConstraint_3=b.asm.qq).apply(null,arguments)},Ds=b._emscripten_bind_btGeneric6DofSpringConstraint_btGeneric6DofSpringConstraint_5=function(){return(Ds=b._emscripten_bind_btGeneric6DofSpringConstraint_btGeneric6DofSpringConstraint_5=b.asm.rq).apply(null,arguments)},Es=b._emscripten_bind_btGeneric6DofSpringConstraint_enableSpring_2=function(){return(Es=b._emscripten_bind_btGeneric6DofSpringConstraint_enableSpring_2= +b.asm.sq).apply(null,arguments)},Fs=b._emscripten_bind_btGeneric6DofSpringConstraint_setStiffness_2=function(){return(Fs=b._emscripten_bind_btGeneric6DofSpringConstraint_setStiffness_2=b.asm.tq).apply(null,arguments)},Gs=b._emscripten_bind_btGeneric6DofSpringConstraint_setDamping_2=function(){return(Gs=b._emscripten_bind_btGeneric6DofSpringConstraint_setDamping_2=b.asm.uq).apply(null,arguments)},Hs=b._emscripten_bind_btGeneric6DofSpringConstraint_setEquilibriumPoint_0=function(){return(Hs=b._emscripten_bind_btGeneric6DofSpringConstraint_setEquilibriumPoint_0= +b.asm.vq).apply(null,arguments)},Is=b._emscripten_bind_btGeneric6DofSpringConstraint_setEquilibriumPoint_1=function(){return(Is=b._emscripten_bind_btGeneric6DofSpringConstraint_setEquilibriumPoint_1=b.asm.wq).apply(null,arguments)},Js=b._emscripten_bind_btGeneric6DofSpringConstraint_setEquilibriumPoint_2=function(){return(Js=b._emscripten_bind_btGeneric6DofSpringConstraint_setEquilibriumPoint_2=b.asm.xq).apply(null,arguments)},Ks=b._emscripten_bind_btGeneric6DofSpringConstraint_setLinearLowerLimit_1= +function(){return(Ks=b._emscripten_bind_btGeneric6DofSpringConstraint_setLinearLowerLimit_1=b.asm.yq).apply(null,arguments)},Ls=b._emscripten_bind_btGeneric6DofSpringConstraint_setLinearUpperLimit_1=function(){return(Ls=b._emscripten_bind_btGeneric6DofSpringConstraint_setLinearUpperLimit_1=b.asm.zq).apply(null,arguments)},Ms=b._emscripten_bind_btGeneric6DofSpringConstraint_setAngularLowerLimit_1=function(){return(Ms=b._emscripten_bind_btGeneric6DofSpringConstraint_setAngularLowerLimit_1=b.asm.Aq).apply(null, +arguments)},Ns=b._emscripten_bind_btGeneric6DofSpringConstraint_setAngularUpperLimit_1=function(){return(Ns=b._emscripten_bind_btGeneric6DofSpringConstraint_setAngularUpperLimit_1=b.asm.Bq).apply(null,arguments)},Os=b._emscripten_bind_btGeneric6DofSpringConstraint_getFrameOffsetA_0=function(){return(Os=b._emscripten_bind_btGeneric6DofSpringConstraint_getFrameOffsetA_0=b.asm.Cq).apply(null,arguments)},Ps=b._emscripten_bind_btGeneric6DofSpringConstraint_enableFeedback_1=function(){return(Ps=b._emscripten_bind_btGeneric6DofSpringConstraint_enableFeedback_1= +b.asm.Dq).apply(null,arguments)},Qs=b._emscripten_bind_btGeneric6DofSpringConstraint_getBreakingImpulseThreshold_0=function(){return(Qs=b._emscripten_bind_btGeneric6DofSpringConstraint_getBreakingImpulseThreshold_0=b.asm.Eq).apply(null,arguments)},Rs=b._emscripten_bind_btGeneric6DofSpringConstraint_setBreakingImpulseThreshold_1=function(){return(Rs=b._emscripten_bind_btGeneric6DofSpringConstraint_setBreakingImpulseThreshold_1=b.asm.Fq).apply(null,arguments)},Ss=b._emscripten_bind_btGeneric6DofSpringConstraint_getParam_2= +function(){return(Ss=b._emscripten_bind_btGeneric6DofSpringConstraint_getParam_2=b.asm.Gq).apply(null,arguments)},Ts=b._emscripten_bind_btGeneric6DofSpringConstraint_setParam_3=function(){return(Ts=b._emscripten_bind_btGeneric6DofSpringConstraint_setParam_3=b.asm.Hq).apply(null,arguments)},Us=b._emscripten_bind_btGeneric6DofSpringConstraint___destroy___0=function(){return(Us=b._emscripten_bind_btGeneric6DofSpringConstraint___destroy___0=b.asm.Iq).apply(null,arguments)},Vs=b._emscripten_bind_btSequentialImpulseConstraintSolver_btSequentialImpulseConstraintSolver_0= +function(){return(Vs=b._emscripten_bind_btSequentialImpulseConstraintSolver_btSequentialImpulseConstraintSolver_0=b.asm.Jq).apply(null,arguments)},Ws=b._emscripten_bind_btSequentialImpulseConstraintSolver___destroy___0=function(){return(Ws=b._emscripten_bind_btSequentialImpulseConstraintSolver___destroy___0=b.asm.Kq).apply(null,arguments)},Xs=b._emscripten_bind_btConeTwistConstraint_btConeTwistConstraint_2=function(){return(Xs=b._emscripten_bind_btConeTwistConstraint_btConeTwistConstraint_2=b.asm.Lq).apply(null, +arguments)},Ys=b._emscripten_bind_btConeTwistConstraint_btConeTwistConstraint_4=function(){return(Ys=b._emscripten_bind_btConeTwistConstraint_btConeTwistConstraint_4=b.asm.Mq).apply(null,arguments)},Zs=b._emscripten_bind_btConeTwistConstraint_setLimit_2=function(){return(Zs=b._emscripten_bind_btConeTwistConstraint_setLimit_2=b.asm.Nq).apply(null,arguments)},$s=b._emscripten_bind_btConeTwistConstraint_setAngularOnly_1=function(){return($s=b._emscripten_bind_btConeTwistConstraint_setAngularOnly_1=b.asm.Oq).apply(null, +arguments)},at=b._emscripten_bind_btConeTwistConstraint_setDamping_1=function(){return(at=b._emscripten_bind_btConeTwistConstraint_setDamping_1=b.asm.Pq).apply(null,arguments)},bt=b._emscripten_bind_btConeTwistConstraint_enableMotor_1=function(){return(bt=b._emscripten_bind_btConeTwistConstraint_enableMotor_1=b.asm.Qq).apply(null,arguments)},ct=b._emscripten_bind_btConeTwistConstraint_setMaxMotorImpulse_1=function(){return(ct=b._emscripten_bind_btConeTwistConstraint_setMaxMotorImpulse_1=b.asm.Rq).apply(null, +arguments)},dt=b._emscripten_bind_btConeTwistConstraint_setMaxMotorImpulseNormalized_1=function(){return(dt=b._emscripten_bind_btConeTwistConstraint_setMaxMotorImpulseNormalized_1=b.asm.Sq).apply(null,arguments)},et=b._emscripten_bind_btConeTwistConstraint_setMotorTarget_1=function(){return(et=b._emscripten_bind_btConeTwistConstraint_setMotorTarget_1=b.asm.Tq).apply(null,arguments)},ft=b._emscripten_bind_btConeTwistConstraint_setMotorTargetInConstraintSpace_1=function(){return(ft=b._emscripten_bind_btConeTwistConstraint_setMotorTargetInConstraintSpace_1= +b.asm.Uq).apply(null,arguments)},gt=b._emscripten_bind_btConeTwistConstraint_enableFeedback_1=function(){return(gt=b._emscripten_bind_btConeTwistConstraint_enableFeedback_1=b.asm.Vq).apply(null,arguments)},ht=b._emscripten_bind_btConeTwistConstraint_getBreakingImpulseThreshold_0=function(){return(ht=b._emscripten_bind_btConeTwistConstraint_getBreakingImpulseThreshold_0=b.asm.Wq).apply(null,arguments)},it=b._emscripten_bind_btConeTwistConstraint_setBreakingImpulseThreshold_1=function(){return(it=b._emscripten_bind_btConeTwistConstraint_setBreakingImpulseThreshold_1= +b.asm.Xq).apply(null,arguments)},jt=b._emscripten_bind_btConeTwistConstraint_getParam_2=function(){return(jt=b._emscripten_bind_btConeTwistConstraint_getParam_2=b.asm.Yq).apply(null,arguments)},kt=b._emscripten_bind_btConeTwistConstraint_setParam_3=function(){return(kt=b._emscripten_bind_btConeTwistConstraint_setParam_3=b.asm.Zq).apply(null,arguments)},lt=b._emscripten_bind_btConeTwistConstraint___destroy___0=function(){return(lt=b._emscripten_bind_btConeTwistConstraint___destroy___0=b.asm._q).apply(null, +arguments)},mt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_2=function(){return(mt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_2=b.asm.$q).apply(null,arguments)},nt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_3=function(){return(nt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_3=b.asm.ar).apply(null,arguments)},ot=b._emscripten_bind_btHingeConstraint_btHingeConstraint_4=function(){return(ot=b._emscripten_bind_btHingeConstraint_btHingeConstraint_4=b.asm.br).apply(null, +arguments)},pt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_5=function(){return(pt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_5=b.asm.cr).apply(null,arguments)},qt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_6=function(){return(qt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_6=b.asm.dr).apply(null,arguments)},rt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_7=function(){return(rt=b._emscripten_bind_btHingeConstraint_btHingeConstraint_7=b.asm.er).apply(null, +arguments)},st=b._emscripten_bind_btHingeConstraint_getHingeAngle_0=function(){return(st=b._emscripten_bind_btHingeConstraint_getHingeAngle_0=b.asm.fr).apply(null,arguments)},tt=b._emscripten_bind_btHingeConstraint_setLimit_4=function(){return(tt=b._emscripten_bind_btHingeConstraint_setLimit_4=b.asm.gr).apply(null,arguments)},ut=b._emscripten_bind_btHingeConstraint_setLimit_5=function(){return(ut=b._emscripten_bind_btHingeConstraint_setLimit_5=b.asm.hr).apply(null,arguments)},vt=b._emscripten_bind_btHingeConstraint_enableAngularMotor_3= +function(){return(vt=b._emscripten_bind_btHingeConstraint_enableAngularMotor_3=b.asm.ir).apply(null,arguments)},wt=b._emscripten_bind_btHingeConstraint_setAngularOnly_1=function(){return(wt=b._emscripten_bind_btHingeConstraint_setAngularOnly_1=b.asm.jr).apply(null,arguments)},xt=b._emscripten_bind_btHingeConstraint_enableMotor_1=function(){return(xt=b._emscripten_bind_btHingeConstraint_enableMotor_1=b.asm.kr).apply(null,arguments)},yt=b._emscripten_bind_btHingeConstraint_setMaxMotorImpulse_1=function(){return(yt= +b._emscripten_bind_btHingeConstraint_setMaxMotorImpulse_1=b.asm.lr).apply(null,arguments)},zt=b._emscripten_bind_btHingeConstraint_setMotorTarget_2=function(){return(zt=b._emscripten_bind_btHingeConstraint_setMotorTarget_2=b.asm.mr).apply(null,arguments)},At=b._emscripten_bind_btHingeConstraint_enableFeedback_1=function(){return(At=b._emscripten_bind_btHingeConstraint_enableFeedback_1=b.asm.nr).apply(null,arguments)},Bt=b._emscripten_bind_btHingeConstraint_getBreakingImpulseThreshold_0=function(){return(Bt= +b._emscripten_bind_btHingeConstraint_getBreakingImpulseThreshold_0=b.asm.or).apply(null,arguments)},Ct=b._emscripten_bind_btHingeConstraint_setBreakingImpulseThreshold_1=function(){return(Ct=b._emscripten_bind_btHingeConstraint_setBreakingImpulseThreshold_1=b.asm.pr).apply(null,arguments)},Dt=b._emscripten_bind_btHingeConstraint_getParam_2=function(){return(Dt=b._emscripten_bind_btHingeConstraint_getParam_2=b.asm.qr).apply(null,arguments)},Et=b._emscripten_bind_btHingeConstraint_setParam_3=function(){return(Et= +b._emscripten_bind_btHingeConstraint_setParam_3=b.asm.rr).apply(null,arguments)},Ft=b._emscripten_bind_btHingeConstraint___destroy___0=function(){return(Ft=b._emscripten_bind_btHingeConstraint___destroy___0=b.asm.sr).apply(null,arguments)},Gt=b._emscripten_bind_btSliderConstraint_btSliderConstraint_3=function(){return(Gt=b._emscripten_bind_btSliderConstraint_btSliderConstraint_3=b.asm.tr).apply(null,arguments)},Ht=b._emscripten_bind_btSliderConstraint_btSliderConstraint_5=function(){return(Ht=b._emscripten_bind_btSliderConstraint_btSliderConstraint_5= +b.asm.ur).apply(null,arguments)},It=b._emscripten_bind_btSliderConstraint_getLinearPos_0=function(){return(It=b._emscripten_bind_btSliderConstraint_getLinearPos_0=b.asm.vr).apply(null,arguments)},Jt=b._emscripten_bind_btSliderConstraint_getAngularPos_0=function(){return(Jt=b._emscripten_bind_btSliderConstraint_getAngularPos_0=b.asm.wr).apply(null,arguments)},Kt=b._emscripten_bind_btSliderConstraint_setLowerLinLimit_1=function(){return(Kt=b._emscripten_bind_btSliderConstraint_setLowerLinLimit_1=b.asm.xr).apply(null, +arguments)},Lt=b._emscripten_bind_btSliderConstraint_setUpperLinLimit_1=function(){return(Lt=b._emscripten_bind_btSliderConstraint_setUpperLinLimit_1=b.asm.yr).apply(null,arguments)},Mt=b._emscripten_bind_btSliderConstraint_setLowerAngLimit_1=function(){return(Mt=b._emscripten_bind_btSliderConstraint_setLowerAngLimit_1=b.asm.zr).apply(null,arguments)},Nt=b._emscripten_bind_btSliderConstraint_setUpperAngLimit_1=function(){return(Nt=b._emscripten_bind_btSliderConstraint_setUpperAngLimit_1=b.asm.Ar).apply(null, +arguments)},Ot=b._emscripten_bind_btSliderConstraint_setPoweredLinMotor_1=function(){return(Ot=b._emscripten_bind_btSliderConstraint_setPoweredLinMotor_1=b.asm.Br).apply(null,arguments)},Pt=b._emscripten_bind_btSliderConstraint_setMaxLinMotorForce_1=function(){return(Pt=b._emscripten_bind_btSliderConstraint_setMaxLinMotorForce_1=b.asm.Cr).apply(null,arguments)},Qt=b._emscripten_bind_btSliderConstraint_setTargetLinMotorVelocity_1=function(){return(Qt=b._emscripten_bind_btSliderConstraint_setTargetLinMotorVelocity_1= +b.asm.Dr).apply(null,arguments)},Rt=b._emscripten_bind_btSliderConstraint_enableFeedback_1=function(){return(Rt=b._emscripten_bind_btSliderConstraint_enableFeedback_1=b.asm.Er).apply(null,arguments)},St=b._emscripten_bind_btSliderConstraint_getBreakingImpulseThreshold_0=function(){return(St=b._emscripten_bind_btSliderConstraint_getBreakingImpulseThreshold_0=b.asm.Fr).apply(null,arguments)},Tt=b._emscripten_bind_btSliderConstraint_setBreakingImpulseThreshold_1=function(){return(Tt=b._emscripten_bind_btSliderConstraint_setBreakingImpulseThreshold_1= +b.asm.Gr).apply(null,arguments)},Ut=b._emscripten_bind_btSliderConstraint_getParam_2=function(){return(Ut=b._emscripten_bind_btSliderConstraint_getParam_2=b.asm.Hr).apply(null,arguments)},Vt=b._emscripten_bind_btSliderConstraint_setParam_3=function(){return(Vt=b._emscripten_bind_btSliderConstraint_setParam_3=b.asm.Ir).apply(null,arguments)},Wt=b._emscripten_bind_btSliderConstraint___destroy___0=function(){return(Wt=b._emscripten_bind_btSliderConstraint___destroy___0=b.asm.Jr).apply(null,arguments)}, +Xt=b._emscripten_bind_btFixedConstraint_btFixedConstraint_4=function(){return(Xt=b._emscripten_bind_btFixedConstraint_btFixedConstraint_4=b.asm.Kr).apply(null,arguments)},Yt=b._emscripten_bind_btFixedConstraint_enableFeedback_1=function(){return(Yt=b._emscripten_bind_btFixedConstraint_enableFeedback_1=b.asm.Lr).apply(null,arguments)},Zt=b._emscripten_bind_btFixedConstraint_getBreakingImpulseThreshold_0=function(){return(Zt=b._emscripten_bind_btFixedConstraint_getBreakingImpulseThreshold_0=b.asm.Mr).apply(null, +arguments)},$t=b._emscripten_bind_btFixedConstraint_setBreakingImpulseThreshold_1=function(){return($t=b._emscripten_bind_btFixedConstraint_setBreakingImpulseThreshold_1=b.asm.Nr).apply(null,arguments)},au=b._emscripten_bind_btFixedConstraint_getParam_2=function(){return(au=b._emscripten_bind_btFixedConstraint_getParam_2=b.asm.Or).apply(null,arguments)},bu=b._emscripten_bind_btFixedConstraint_setParam_3=function(){return(bu=b._emscripten_bind_btFixedConstraint_setParam_3=b.asm.Pr).apply(null,arguments)}, +cu=b._emscripten_bind_btFixedConstraint___destroy___0=function(){return(cu=b._emscripten_bind_btFixedConstraint___destroy___0=b.asm.Qr).apply(null,arguments)},du=b._emscripten_bind_btConstraintSolver___destroy___0=function(){return(du=b._emscripten_bind_btConstraintSolver___destroy___0=b.asm.Rr).apply(null,arguments)},eu=b._emscripten_bind_btDispatcherInfo_get_m_timeStep_0=function(){return(eu=b._emscripten_bind_btDispatcherInfo_get_m_timeStep_0=b.asm.Sr).apply(null,arguments)},fu=b._emscripten_bind_btDispatcherInfo_set_m_timeStep_1= +function(){return(fu=b._emscripten_bind_btDispatcherInfo_set_m_timeStep_1=b.asm.Tr).apply(null,arguments)},gu=b._emscripten_bind_btDispatcherInfo_get_m_stepCount_0=function(){return(gu=b._emscripten_bind_btDispatcherInfo_get_m_stepCount_0=b.asm.Ur).apply(null,arguments)},hu=b._emscripten_bind_btDispatcherInfo_set_m_stepCount_1=function(){return(hu=b._emscripten_bind_btDispatcherInfo_set_m_stepCount_1=b.asm.Vr).apply(null,arguments)},iu=b._emscripten_bind_btDispatcherInfo_get_m_dispatchFunc_0=function(){return(iu= +b._emscripten_bind_btDispatcherInfo_get_m_dispatchFunc_0=b.asm.Wr).apply(null,arguments)},ju=b._emscripten_bind_btDispatcherInfo_set_m_dispatchFunc_1=function(){return(ju=b._emscripten_bind_btDispatcherInfo_set_m_dispatchFunc_1=b.asm.Xr).apply(null,arguments)},ku=b._emscripten_bind_btDispatcherInfo_get_m_timeOfImpact_0=function(){return(ku=b._emscripten_bind_btDispatcherInfo_get_m_timeOfImpact_0=b.asm.Yr).apply(null,arguments)},lu=b._emscripten_bind_btDispatcherInfo_set_m_timeOfImpact_1=function(){return(lu= +b._emscripten_bind_btDispatcherInfo_set_m_timeOfImpact_1=b.asm.Zr).apply(null,arguments)},mu=b._emscripten_bind_btDispatcherInfo_get_m_useContinuous_0=function(){return(mu=b._emscripten_bind_btDispatcherInfo_get_m_useContinuous_0=b.asm._r).apply(null,arguments)},nu=b._emscripten_bind_btDispatcherInfo_set_m_useContinuous_1=function(){return(nu=b._emscripten_bind_btDispatcherInfo_set_m_useContinuous_1=b.asm.$r).apply(null,arguments)},ou=b._emscripten_bind_btDispatcherInfo_get_m_enableSatConvex_0=function(){return(ou= +b._emscripten_bind_btDispatcherInfo_get_m_enableSatConvex_0=b.asm.as).apply(null,arguments)},pu=b._emscripten_bind_btDispatcherInfo_set_m_enableSatConvex_1=function(){return(pu=b._emscripten_bind_btDispatcherInfo_set_m_enableSatConvex_1=b.asm.bs).apply(null,arguments)},qu=b._emscripten_bind_btDispatcherInfo_get_m_enableSPU_0=function(){return(qu=b._emscripten_bind_btDispatcherInfo_get_m_enableSPU_0=b.asm.cs).apply(null,arguments)},ru=b._emscripten_bind_btDispatcherInfo_set_m_enableSPU_1=function(){return(ru= +b._emscripten_bind_btDispatcherInfo_set_m_enableSPU_1=b.asm.ds).apply(null,arguments)},su=b._emscripten_bind_btDispatcherInfo_get_m_useEpa_0=function(){return(su=b._emscripten_bind_btDispatcherInfo_get_m_useEpa_0=b.asm.es).apply(null,arguments)},tu=b._emscripten_bind_btDispatcherInfo_set_m_useEpa_1=function(){return(tu=b._emscripten_bind_btDispatcherInfo_set_m_useEpa_1=b.asm.fs).apply(null,arguments)},uu=b._emscripten_bind_btDispatcherInfo_get_m_allowedCcdPenetration_0=function(){return(uu=b._emscripten_bind_btDispatcherInfo_get_m_allowedCcdPenetration_0= +b.asm.gs).apply(null,arguments)},vu=b._emscripten_bind_btDispatcherInfo_set_m_allowedCcdPenetration_1=function(){return(vu=b._emscripten_bind_btDispatcherInfo_set_m_allowedCcdPenetration_1=b.asm.hs).apply(null,arguments)},wu=b._emscripten_bind_btDispatcherInfo_get_m_useConvexConservativeDistanceUtil_0=function(){return(wu=b._emscripten_bind_btDispatcherInfo_get_m_useConvexConservativeDistanceUtil_0=b.asm.is).apply(null,arguments)},xu=b._emscripten_bind_btDispatcherInfo_set_m_useConvexConservativeDistanceUtil_1= +function(){return(xu=b._emscripten_bind_btDispatcherInfo_set_m_useConvexConservativeDistanceUtil_1=b.asm.js).apply(null,arguments)},yu=b._emscripten_bind_btDispatcherInfo_get_m_convexConservativeDistanceThreshold_0=function(){return(yu=b._emscripten_bind_btDispatcherInfo_get_m_convexConservativeDistanceThreshold_0=b.asm.ks).apply(null,arguments)},zu=b._emscripten_bind_btDispatcherInfo_set_m_convexConservativeDistanceThreshold_1=function(){return(zu=b._emscripten_bind_btDispatcherInfo_set_m_convexConservativeDistanceThreshold_1= +b.asm.ls).apply(null,arguments)},Au=b._emscripten_bind_btDispatcherInfo___destroy___0=function(){return(Au=b._emscripten_bind_btDispatcherInfo___destroy___0=b.asm.ms).apply(null,arguments)},Bu=b._emscripten_bind_btContactSolverInfo_get_m_splitImpulse_0=function(){return(Bu=b._emscripten_bind_btContactSolverInfo_get_m_splitImpulse_0=b.asm.ns).apply(null,arguments)},Cu=b._emscripten_bind_btContactSolverInfo_set_m_splitImpulse_1=function(){return(Cu=b._emscripten_bind_btContactSolverInfo_set_m_splitImpulse_1= +b.asm.os).apply(null,arguments)},Du=b._emscripten_bind_btContactSolverInfo_get_m_splitImpulsePenetrationThreshold_0=function(){return(Du=b._emscripten_bind_btContactSolverInfo_get_m_splitImpulsePenetrationThreshold_0=b.asm.ps).apply(null,arguments)},Eu=b._emscripten_bind_btContactSolverInfo_set_m_splitImpulsePenetrationThreshold_1=function(){return(Eu=b._emscripten_bind_btContactSolverInfo_set_m_splitImpulsePenetrationThreshold_1=b.asm.qs).apply(null,arguments)},Fu=b._emscripten_bind_btContactSolverInfo_get_m_numIterations_0= +function(){return(Fu=b._emscripten_bind_btContactSolverInfo_get_m_numIterations_0=b.asm.rs).apply(null,arguments)},Gu=b._emscripten_bind_btContactSolverInfo_set_m_numIterations_1=function(){return(Gu=b._emscripten_bind_btContactSolverInfo_set_m_numIterations_1=b.asm.ss).apply(null,arguments)},Hu=b._emscripten_bind_btContactSolverInfo___destroy___0=function(){return(Hu=b._emscripten_bind_btContactSolverInfo___destroy___0=b.asm.ts).apply(null,arguments)},Iu=b._emscripten_bind_btVehicleTuning_btVehicleTuning_0= +function(){return(Iu=b._emscripten_bind_btVehicleTuning_btVehicleTuning_0=b.asm.us).apply(null,arguments)},Ju=b._emscripten_bind_btVehicleTuning_get_m_suspensionStiffness_0=function(){return(Ju=b._emscripten_bind_btVehicleTuning_get_m_suspensionStiffness_0=b.asm.vs).apply(null,arguments)},Ku=b._emscripten_bind_btVehicleTuning_set_m_suspensionStiffness_1=function(){return(Ku=b._emscripten_bind_btVehicleTuning_set_m_suspensionStiffness_1=b.asm.ws).apply(null,arguments)},Lu=b._emscripten_bind_btVehicleTuning_get_m_suspensionCompression_0= +function(){return(Lu=b._emscripten_bind_btVehicleTuning_get_m_suspensionCompression_0=b.asm.xs).apply(null,arguments)},Mu=b._emscripten_bind_btVehicleTuning_set_m_suspensionCompression_1=function(){return(Mu=b._emscripten_bind_btVehicleTuning_set_m_suspensionCompression_1=b.asm.ys).apply(null,arguments)},Nu=b._emscripten_bind_btVehicleTuning_get_m_suspensionDamping_0=function(){return(Nu=b._emscripten_bind_btVehicleTuning_get_m_suspensionDamping_0=b.asm.zs).apply(null,arguments)},Ou=b._emscripten_bind_btVehicleTuning_set_m_suspensionDamping_1= +function(){return(Ou=b._emscripten_bind_btVehicleTuning_set_m_suspensionDamping_1=b.asm.As).apply(null,arguments)},Pu=b._emscripten_bind_btVehicleTuning_get_m_maxSuspensionTravelCm_0=function(){return(Pu=b._emscripten_bind_btVehicleTuning_get_m_maxSuspensionTravelCm_0=b.asm.Bs).apply(null,arguments)},Qu=b._emscripten_bind_btVehicleTuning_set_m_maxSuspensionTravelCm_1=function(){return(Qu=b._emscripten_bind_btVehicleTuning_set_m_maxSuspensionTravelCm_1=b.asm.Cs).apply(null,arguments)},Ru=b._emscripten_bind_btVehicleTuning_get_m_frictionSlip_0= +function(){return(Ru=b._emscripten_bind_btVehicleTuning_get_m_frictionSlip_0=b.asm.Ds).apply(null,arguments)},Su=b._emscripten_bind_btVehicleTuning_set_m_frictionSlip_1=function(){return(Su=b._emscripten_bind_btVehicleTuning_set_m_frictionSlip_1=b.asm.Es).apply(null,arguments)},Tu=b._emscripten_bind_btVehicleTuning_get_m_maxSuspensionForce_0=function(){return(Tu=b._emscripten_bind_btVehicleTuning_get_m_maxSuspensionForce_0=b.asm.Fs).apply(null,arguments)},Uu=b._emscripten_bind_btVehicleTuning_set_m_maxSuspensionForce_1= +function(){return(Uu=b._emscripten_bind_btVehicleTuning_set_m_maxSuspensionForce_1=b.asm.Gs).apply(null,arguments)},Vu=b._emscripten_bind_btVehicleRaycasterResult_get_m_hitPointInWorld_0=function(){return(Vu=b._emscripten_bind_btVehicleRaycasterResult_get_m_hitPointInWorld_0=b.asm.Hs).apply(null,arguments)},Wu=b._emscripten_bind_btVehicleRaycasterResult_set_m_hitPointInWorld_1=function(){return(Wu=b._emscripten_bind_btVehicleRaycasterResult_set_m_hitPointInWorld_1=b.asm.Is).apply(null,arguments)}, +Xu=b._emscripten_bind_btVehicleRaycasterResult_get_m_hitNormalInWorld_0=function(){return(Xu=b._emscripten_bind_btVehicleRaycasterResult_get_m_hitNormalInWorld_0=b.asm.Js).apply(null,arguments)},Yu=b._emscripten_bind_btVehicleRaycasterResult_set_m_hitNormalInWorld_1=function(){return(Yu=b._emscripten_bind_btVehicleRaycasterResult_set_m_hitNormalInWorld_1=b.asm.Ks).apply(null,arguments)},Zu=b._emscripten_bind_btVehicleRaycasterResult_get_m_distFraction_0=function(){return(Zu=b._emscripten_bind_btVehicleRaycasterResult_get_m_distFraction_0= +b.asm.Ls).apply(null,arguments)},$u=b._emscripten_bind_btVehicleRaycasterResult_set_m_distFraction_1=function(){return($u=b._emscripten_bind_btVehicleRaycasterResult_set_m_distFraction_1=b.asm.Ms).apply(null,arguments)},av=b._emscripten_bind_btVehicleRaycasterResult___destroy___0=function(){return(av=b._emscripten_bind_btVehicleRaycasterResult___destroy___0=b.asm.Ns).apply(null,arguments)},bv=b._emscripten_bind_btDefaultVehicleRaycaster_btDefaultVehicleRaycaster_1=function(){return(bv=b._emscripten_bind_btDefaultVehicleRaycaster_btDefaultVehicleRaycaster_1= +b.asm.Os).apply(null,arguments)},cv=b._emscripten_bind_btDefaultVehicleRaycaster_castRay_3=function(){return(cv=b._emscripten_bind_btDefaultVehicleRaycaster_castRay_3=b.asm.Ps).apply(null,arguments)},dv=b._emscripten_bind_btDefaultVehicleRaycaster___destroy___0=function(){return(dv=b._emscripten_bind_btDefaultVehicleRaycaster___destroy___0=b.asm.Qs).apply(null,arguments)},ev=b._emscripten_bind_RaycastInfo_get_m_contactNormalWS_0=function(){return(ev=b._emscripten_bind_RaycastInfo_get_m_contactNormalWS_0= +b.asm.Rs).apply(null,arguments)},fv=b._emscripten_bind_RaycastInfo_set_m_contactNormalWS_1=function(){return(fv=b._emscripten_bind_RaycastInfo_set_m_contactNormalWS_1=b.asm.Ss).apply(null,arguments)},gv=b._emscripten_bind_RaycastInfo_get_m_contactPointWS_0=function(){return(gv=b._emscripten_bind_RaycastInfo_get_m_contactPointWS_0=b.asm.Ts).apply(null,arguments)},hv=b._emscripten_bind_RaycastInfo_set_m_contactPointWS_1=function(){return(hv=b._emscripten_bind_RaycastInfo_set_m_contactPointWS_1=b.asm.Us).apply(null, +arguments)},iv=b._emscripten_bind_RaycastInfo_get_m_suspensionLength_0=function(){return(iv=b._emscripten_bind_RaycastInfo_get_m_suspensionLength_0=b.asm.Vs).apply(null,arguments)},jv=b._emscripten_bind_RaycastInfo_set_m_suspensionLength_1=function(){return(jv=b._emscripten_bind_RaycastInfo_set_m_suspensionLength_1=b.asm.Ws).apply(null,arguments)},kv=b._emscripten_bind_RaycastInfo_get_m_hardPointWS_0=function(){return(kv=b._emscripten_bind_RaycastInfo_get_m_hardPointWS_0=b.asm.Xs).apply(null,arguments)}, +lv=b._emscripten_bind_RaycastInfo_set_m_hardPointWS_1=function(){return(lv=b._emscripten_bind_RaycastInfo_set_m_hardPointWS_1=b.asm.Ys).apply(null,arguments)},mv=b._emscripten_bind_RaycastInfo_get_m_wheelDirectionWS_0=function(){return(mv=b._emscripten_bind_RaycastInfo_get_m_wheelDirectionWS_0=b.asm.Zs).apply(null,arguments)},nv=b._emscripten_bind_RaycastInfo_set_m_wheelDirectionWS_1=function(){return(nv=b._emscripten_bind_RaycastInfo_set_m_wheelDirectionWS_1=b.asm._s).apply(null,arguments)},ov=b._emscripten_bind_RaycastInfo_get_m_wheelAxleWS_0= +function(){return(ov=b._emscripten_bind_RaycastInfo_get_m_wheelAxleWS_0=b.asm.$s).apply(null,arguments)},pv=b._emscripten_bind_RaycastInfo_set_m_wheelAxleWS_1=function(){return(pv=b._emscripten_bind_RaycastInfo_set_m_wheelAxleWS_1=b.asm.at).apply(null,arguments)},qv=b._emscripten_bind_RaycastInfo_get_m_isInContact_0=function(){return(qv=b._emscripten_bind_RaycastInfo_get_m_isInContact_0=b.asm.bt).apply(null,arguments)},rv=b._emscripten_bind_RaycastInfo_set_m_isInContact_1=function(){return(rv=b._emscripten_bind_RaycastInfo_set_m_isInContact_1= +b.asm.ct).apply(null,arguments)},sv=b._emscripten_bind_RaycastInfo_get_m_groundObject_0=function(){return(sv=b._emscripten_bind_RaycastInfo_get_m_groundObject_0=b.asm.dt).apply(null,arguments)},tv=b._emscripten_bind_RaycastInfo_set_m_groundObject_1=function(){return(tv=b._emscripten_bind_RaycastInfo_set_m_groundObject_1=b.asm.et).apply(null,arguments)},uv=b._emscripten_bind_RaycastInfo___destroy___0=function(){return(uv=b._emscripten_bind_RaycastInfo___destroy___0=b.asm.ft).apply(null,arguments)}, +vv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_chassisConnectionCS_0=function(){return(vv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_chassisConnectionCS_0=b.asm.gt).apply(null,arguments)},wv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_chassisConnectionCS_1=function(){return(wv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_chassisConnectionCS_1=b.asm.ht).apply(null,arguments)},xv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelDirectionCS_0=function(){return(xv= +b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelDirectionCS_0=b.asm.it).apply(null,arguments)},yv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelDirectionCS_1=function(){return(yv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelDirectionCS_1=b.asm.jt).apply(null,arguments)},zv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelAxleCS_0=function(){return(zv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelAxleCS_0=b.asm.kt).apply(null,arguments)},Av=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelAxleCS_1= +function(){return(Av=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelAxleCS_1=b.asm.lt).apply(null,arguments)},Bv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_suspensionRestLength_0=function(){return(Bv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_suspensionRestLength_0=b.asm.mt).apply(null,arguments)},Cv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_suspensionRestLength_1=function(){return(Cv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_suspensionRestLength_1= +b.asm.nt).apply(null,arguments)},Dv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_maxSuspensionTravelCm_0=function(){return(Dv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_maxSuspensionTravelCm_0=b.asm.ot).apply(null,arguments)},Ev=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_maxSuspensionTravelCm_1=function(){return(Ev=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_maxSuspensionTravelCm_1=b.asm.pt).apply(null,arguments)},Fv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelRadius_0= +function(){return(Fv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelRadius_0=b.asm.qt).apply(null,arguments)},Gv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelRadius_1=function(){return(Gv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelRadius_1=b.asm.rt).apply(null,arguments)},Hv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_suspensionStiffness_0=function(){return(Hv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_suspensionStiffness_0=b.asm.st).apply(null, +arguments)},Iv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_suspensionStiffness_1=function(){return(Iv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_suspensionStiffness_1=b.asm.tt).apply(null,arguments)},Jv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelsDampingCompression_0=function(){return(Jv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelsDampingCompression_0=b.asm.ut).apply(null,arguments)},Kv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelsDampingCompression_1= +function(){return(Kv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelsDampingCompression_1=b.asm.vt).apply(null,arguments)},Lv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelsDampingRelaxation_0=function(){return(Lv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_wheelsDampingRelaxation_0=b.asm.wt).apply(null,arguments)},Mv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelsDampingRelaxation_1=function(){return(Mv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_wheelsDampingRelaxation_1= +b.asm.xt).apply(null,arguments)},Nv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_frictionSlip_0=function(){return(Nv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_frictionSlip_0=b.asm.yt).apply(null,arguments)},Ov=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_frictionSlip_1=function(){return(Ov=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_frictionSlip_1=b.asm.zt).apply(null,arguments)},Pv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_maxSuspensionForce_0=function(){return(Pv= +b._emscripten_bind_btWheelInfoConstructionInfo_get_m_maxSuspensionForce_0=b.asm.At).apply(null,arguments)},Qv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_maxSuspensionForce_1=function(){return(Qv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_maxSuspensionForce_1=b.asm.Bt).apply(null,arguments)},Rv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_bIsFrontWheel_0=function(){return(Rv=b._emscripten_bind_btWheelInfoConstructionInfo_get_m_bIsFrontWheel_0=b.asm.Ct).apply(null,arguments)}, +Sv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_bIsFrontWheel_1=function(){return(Sv=b._emscripten_bind_btWheelInfoConstructionInfo_set_m_bIsFrontWheel_1=b.asm.Dt).apply(null,arguments)},Tv=b._emscripten_bind_btWheelInfoConstructionInfo___destroy___0=function(){return(Tv=b._emscripten_bind_btWheelInfoConstructionInfo___destroy___0=b.asm.Et).apply(null,arguments)},Uv=b._emscripten_bind_btWheelInfo_btWheelInfo_1=function(){return(Uv=b._emscripten_bind_btWheelInfo_btWheelInfo_1=b.asm.Ft).apply(null, +arguments)},Vv=b._emscripten_bind_btWheelInfo_getSuspensionRestLength_0=function(){return(Vv=b._emscripten_bind_btWheelInfo_getSuspensionRestLength_0=b.asm.Gt).apply(null,arguments)},Wv=b._emscripten_bind_btWheelInfo_updateWheel_2=function(){return(Wv=b._emscripten_bind_btWheelInfo_updateWheel_2=b.asm.Ht).apply(null,arguments)},Xv=b._emscripten_bind_btWheelInfo_get_m_suspensionStiffness_0=function(){return(Xv=b._emscripten_bind_btWheelInfo_get_m_suspensionStiffness_0=b.asm.It).apply(null,arguments)}, +Yv=b._emscripten_bind_btWheelInfo_set_m_suspensionStiffness_1=function(){return(Yv=b._emscripten_bind_btWheelInfo_set_m_suspensionStiffness_1=b.asm.Jt).apply(null,arguments)},Zv=b._emscripten_bind_btWheelInfo_get_m_frictionSlip_0=function(){return(Zv=b._emscripten_bind_btWheelInfo_get_m_frictionSlip_0=b.asm.Kt).apply(null,arguments)},$v=b._emscripten_bind_btWheelInfo_set_m_frictionSlip_1=function(){return($v=b._emscripten_bind_btWheelInfo_set_m_frictionSlip_1=b.asm.Lt).apply(null,arguments)},aw=b._emscripten_bind_btWheelInfo_get_m_engineForce_0= +function(){return(aw=b._emscripten_bind_btWheelInfo_get_m_engineForce_0=b.asm.Mt).apply(null,arguments)},bw=b._emscripten_bind_btWheelInfo_set_m_engineForce_1=function(){return(bw=b._emscripten_bind_btWheelInfo_set_m_engineForce_1=b.asm.Nt).apply(null,arguments)},cw=b._emscripten_bind_btWheelInfo_get_m_rollInfluence_0=function(){return(cw=b._emscripten_bind_btWheelInfo_get_m_rollInfluence_0=b.asm.Ot).apply(null,arguments)},dw=b._emscripten_bind_btWheelInfo_set_m_rollInfluence_1=function(){return(dw= +b._emscripten_bind_btWheelInfo_set_m_rollInfluence_1=b.asm.Pt).apply(null,arguments)},ew=b._emscripten_bind_btWheelInfo_get_m_suspensionRestLength1_0=function(){return(ew=b._emscripten_bind_btWheelInfo_get_m_suspensionRestLength1_0=b.asm.Qt).apply(null,arguments)},fw=b._emscripten_bind_btWheelInfo_set_m_suspensionRestLength1_1=function(){return(fw=b._emscripten_bind_btWheelInfo_set_m_suspensionRestLength1_1=b.asm.Rt).apply(null,arguments)},gw=b._emscripten_bind_btWheelInfo_get_m_wheelsRadius_0=function(){return(gw= +b._emscripten_bind_btWheelInfo_get_m_wheelsRadius_0=b.asm.St).apply(null,arguments)},hw=b._emscripten_bind_btWheelInfo_set_m_wheelsRadius_1=function(){return(hw=b._emscripten_bind_btWheelInfo_set_m_wheelsRadius_1=b.asm.Tt).apply(null,arguments)},iw=b._emscripten_bind_btWheelInfo_get_m_wheelsDampingCompression_0=function(){return(iw=b._emscripten_bind_btWheelInfo_get_m_wheelsDampingCompression_0=b.asm.Ut).apply(null,arguments)},jw=b._emscripten_bind_btWheelInfo_set_m_wheelsDampingCompression_1=function(){return(jw= +b._emscripten_bind_btWheelInfo_set_m_wheelsDampingCompression_1=b.asm.Vt).apply(null,arguments)},kw=b._emscripten_bind_btWheelInfo_get_m_wheelsDampingRelaxation_0=function(){return(kw=b._emscripten_bind_btWheelInfo_get_m_wheelsDampingRelaxation_0=b.asm.Wt).apply(null,arguments)},lw=b._emscripten_bind_btWheelInfo_set_m_wheelsDampingRelaxation_1=function(){return(lw=b._emscripten_bind_btWheelInfo_set_m_wheelsDampingRelaxation_1=b.asm.Xt).apply(null,arguments)},mw=b._emscripten_bind_btWheelInfo_get_m_steering_0= +function(){return(mw=b._emscripten_bind_btWheelInfo_get_m_steering_0=b.asm.Yt).apply(null,arguments)},nw=b._emscripten_bind_btWheelInfo_set_m_steering_1=function(){return(nw=b._emscripten_bind_btWheelInfo_set_m_steering_1=b.asm.Zt).apply(null,arguments)},ow=b._emscripten_bind_btWheelInfo_get_m_maxSuspensionForce_0=function(){return(ow=b._emscripten_bind_btWheelInfo_get_m_maxSuspensionForce_0=b.asm._t).apply(null,arguments)},pw=b._emscripten_bind_btWheelInfo_set_m_maxSuspensionForce_1=function(){return(pw= +b._emscripten_bind_btWheelInfo_set_m_maxSuspensionForce_1=b.asm.$t).apply(null,arguments)},qw=b._emscripten_bind_btWheelInfo_get_m_maxSuspensionTravelCm_0=function(){return(qw=b._emscripten_bind_btWheelInfo_get_m_maxSuspensionTravelCm_0=b.asm.au).apply(null,arguments)},rw=b._emscripten_bind_btWheelInfo_set_m_maxSuspensionTravelCm_1=function(){return(rw=b._emscripten_bind_btWheelInfo_set_m_maxSuspensionTravelCm_1=b.asm.bu).apply(null,arguments)},sw=b._emscripten_bind_btWheelInfo_get_m_wheelsSuspensionForce_0= +function(){return(sw=b._emscripten_bind_btWheelInfo_get_m_wheelsSuspensionForce_0=b.asm.cu).apply(null,arguments)},tw=b._emscripten_bind_btWheelInfo_set_m_wheelsSuspensionForce_1=function(){return(tw=b._emscripten_bind_btWheelInfo_set_m_wheelsSuspensionForce_1=b.asm.du).apply(null,arguments)},uw=b._emscripten_bind_btWheelInfo_get_m_bIsFrontWheel_0=function(){return(uw=b._emscripten_bind_btWheelInfo_get_m_bIsFrontWheel_0=b.asm.eu).apply(null,arguments)},vw=b._emscripten_bind_btWheelInfo_set_m_bIsFrontWheel_1= +function(){return(vw=b._emscripten_bind_btWheelInfo_set_m_bIsFrontWheel_1=b.asm.fu).apply(null,arguments)},ww=b._emscripten_bind_btWheelInfo_get_m_raycastInfo_0=function(){return(ww=b._emscripten_bind_btWheelInfo_get_m_raycastInfo_0=b.asm.gu).apply(null,arguments)},xw=b._emscripten_bind_btWheelInfo_set_m_raycastInfo_1=function(){return(xw=b._emscripten_bind_btWheelInfo_set_m_raycastInfo_1=b.asm.hu).apply(null,arguments)},yw=b._emscripten_bind_btWheelInfo_get_m_chassisConnectionPointCS_0=function(){return(yw= +b._emscripten_bind_btWheelInfo_get_m_chassisConnectionPointCS_0=b.asm.iu).apply(null,arguments)},zw=b._emscripten_bind_btWheelInfo_set_m_chassisConnectionPointCS_1=function(){return(zw=b._emscripten_bind_btWheelInfo_set_m_chassisConnectionPointCS_1=b.asm.ju).apply(null,arguments)},Aw=b._emscripten_bind_btWheelInfo_get_m_worldTransform_0=function(){return(Aw=b._emscripten_bind_btWheelInfo_get_m_worldTransform_0=b.asm.ku).apply(null,arguments)},Bw=b._emscripten_bind_btWheelInfo_set_m_worldTransform_1= +function(){return(Bw=b._emscripten_bind_btWheelInfo_set_m_worldTransform_1=b.asm.lu).apply(null,arguments)},Cw=b._emscripten_bind_btWheelInfo_get_m_wheelDirectionCS_0=function(){return(Cw=b._emscripten_bind_btWheelInfo_get_m_wheelDirectionCS_0=b.asm.mu).apply(null,arguments)},Dw=b._emscripten_bind_btWheelInfo_set_m_wheelDirectionCS_1=function(){return(Dw=b._emscripten_bind_btWheelInfo_set_m_wheelDirectionCS_1=b.asm.nu).apply(null,arguments)},Ew=b._emscripten_bind_btWheelInfo_get_m_wheelAxleCS_0=function(){return(Ew= +b._emscripten_bind_btWheelInfo_get_m_wheelAxleCS_0=b.asm.ou).apply(null,arguments)},Fw=b._emscripten_bind_btWheelInfo_set_m_wheelAxleCS_1=function(){return(Fw=b._emscripten_bind_btWheelInfo_set_m_wheelAxleCS_1=b.asm.pu).apply(null,arguments)},Gw=b._emscripten_bind_btWheelInfo_get_m_rotation_0=function(){return(Gw=b._emscripten_bind_btWheelInfo_get_m_rotation_0=b.asm.qu).apply(null,arguments)},Hw=b._emscripten_bind_btWheelInfo_set_m_rotation_1=function(){return(Hw=b._emscripten_bind_btWheelInfo_set_m_rotation_1= +b.asm.ru).apply(null,arguments)},Iw=b._emscripten_bind_btWheelInfo_get_m_deltaRotation_0=function(){return(Iw=b._emscripten_bind_btWheelInfo_get_m_deltaRotation_0=b.asm.su).apply(null,arguments)},Jw=b._emscripten_bind_btWheelInfo_set_m_deltaRotation_1=function(){return(Jw=b._emscripten_bind_btWheelInfo_set_m_deltaRotation_1=b.asm.tu).apply(null,arguments)},Kw=b._emscripten_bind_btWheelInfo_get_m_brake_0=function(){return(Kw=b._emscripten_bind_btWheelInfo_get_m_brake_0=b.asm.uu).apply(null,arguments)}, +Lw=b._emscripten_bind_btWheelInfo_set_m_brake_1=function(){return(Lw=b._emscripten_bind_btWheelInfo_set_m_brake_1=b.asm.vu).apply(null,arguments)},Mw=b._emscripten_bind_btWheelInfo_get_m_clippedInvContactDotSuspension_0=function(){return(Mw=b._emscripten_bind_btWheelInfo_get_m_clippedInvContactDotSuspension_0=b.asm.wu).apply(null,arguments)},Nw=b._emscripten_bind_btWheelInfo_set_m_clippedInvContactDotSuspension_1=function(){return(Nw=b._emscripten_bind_btWheelInfo_set_m_clippedInvContactDotSuspension_1= +b.asm.xu).apply(null,arguments)},Ow=b._emscripten_bind_btWheelInfo_get_m_suspensionRelativeVelocity_0=function(){return(Ow=b._emscripten_bind_btWheelInfo_get_m_suspensionRelativeVelocity_0=b.asm.yu).apply(null,arguments)},Pw=b._emscripten_bind_btWheelInfo_set_m_suspensionRelativeVelocity_1=function(){return(Pw=b._emscripten_bind_btWheelInfo_set_m_suspensionRelativeVelocity_1=b.asm.zu).apply(null,arguments)},Qw=b._emscripten_bind_btWheelInfo_get_m_skidInfo_0=function(){return(Qw=b._emscripten_bind_btWheelInfo_get_m_skidInfo_0= +b.asm.Au).apply(null,arguments)},Rw=b._emscripten_bind_btWheelInfo_set_m_skidInfo_1=function(){return(Rw=b._emscripten_bind_btWheelInfo_set_m_skidInfo_1=b.asm.Bu).apply(null,arguments)},Sw=b._emscripten_bind_btWheelInfo___destroy___0=function(){return(Sw=b._emscripten_bind_btWheelInfo___destroy___0=b.asm.Cu).apply(null,arguments)},Tw=b._emscripten_bind_btKinematicCharacterController_btKinematicCharacterController_3=function(){return(Tw=b._emscripten_bind_btKinematicCharacterController_btKinematicCharacterController_3= +b.asm.Du).apply(null,arguments)},Uw=b._emscripten_bind_btKinematicCharacterController_btKinematicCharacterController_4=function(){return(Uw=b._emscripten_bind_btKinematicCharacterController_btKinematicCharacterController_4=b.asm.Eu).apply(null,arguments)},Vw=b._emscripten_bind_btKinematicCharacterController_setUpAxis_1=function(){return(Vw=b._emscripten_bind_btKinematicCharacterController_setUpAxis_1=b.asm.Fu).apply(null,arguments)},Ww=b._emscripten_bind_btKinematicCharacterController_setWalkDirection_1= +function(){return(Ww=b._emscripten_bind_btKinematicCharacterController_setWalkDirection_1=b.asm.Gu).apply(null,arguments)},Xw=b._emscripten_bind_btKinematicCharacterController_setVelocityForTimeInterval_2=function(){return(Xw=b._emscripten_bind_btKinematicCharacterController_setVelocityForTimeInterval_2=b.asm.Hu).apply(null,arguments)},Yw=b._emscripten_bind_btKinematicCharacterController_warp_1=function(){return(Yw=b._emscripten_bind_btKinematicCharacterController_warp_1=b.asm.Iu).apply(null,arguments)}, +Zw=b._emscripten_bind_btKinematicCharacterController_preStep_1=function(){return(Zw=b._emscripten_bind_btKinematicCharacterController_preStep_1=b.asm.Ju).apply(null,arguments)},$w=b._emscripten_bind_btKinematicCharacterController_playerStep_2=function(){return($w=b._emscripten_bind_btKinematicCharacterController_playerStep_2=b.asm.Ku).apply(null,arguments)},ax=b._emscripten_bind_btKinematicCharacterController_setFallSpeed_1=function(){return(ax=b._emscripten_bind_btKinematicCharacterController_setFallSpeed_1= +b.asm.Lu).apply(null,arguments)},bx=b._emscripten_bind_btKinematicCharacterController_setJumpSpeed_1=function(){return(bx=b._emscripten_bind_btKinematicCharacterController_setJumpSpeed_1=b.asm.Mu).apply(null,arguments)},cx=b._emscripten_bind_btKinematicCharacterController_setMaxJumpHeight_1=function(){return(cx=b._emscripten_bind_btKinematicCharacterController_setMaxJumpHeight_1=b.asm.Nu).apply(null,arguments)},dx=b._emscripten_bind_btKinematicCharacterController_canJump_0=function(){return(dx=b._emscripten_bind_btKinematicCharacterController_canJump_0= +b.asm.Ou).apply(null,arguments)},ex=b._emscripten_bind_btKinematicCharacterController_jump_0=function(){return(ex=b._emscripten_bind_btKinematicCharacterController_jump_0=b.asm.Pu).apply(null,arguments)},fx=b._emscripten_bind_btKinematicCharacterController_setGravity_1=function(){return(fx=b._emscripten_bind_btKinematicCharacterController_setGravity_1=b.asm.Qu).apply(null,arguments)},gx=b._emscripten_bind_btKinematicCharacterController_getGravity_0=function(){return(gx=b._emscripten_bind_btKinematicCharacterController_getGravity_0= +b.asm.Ru).apply(null,arguments)},hx=b._emscripten_bind_btKinematicCharacterController_setMaxSlope_1=function(){return(hx=b._emscripten_bind_btKinematicCharacterController_setMaxSlope_1=b.asm.Su).apply(null,arguments)},ix=b._emscripten_bind_btKinematicCharacterController_getMaxSlope_0=function(){return(ix=b._emscripten_bind_btKinematicCharacterController_getMaxSlope_0=b.asm.Tu).apply(null,arguments)},jx=b._emscripten_bind_btKinematicCharacterController_getGhostObject_0=function(){return(jx=b._emscripten_bind_btKinematicCharacterController_getGhostObject_0= +b.asm.Uu).apply(null,arguments)},kx=b._emscripten_bind_btKinematicCharacterController_setUseGhostSweepTest_1=function(){return(kx=b._emscripten_bind_btKinematicCharacterController_setUseGhostSweepTest_1=b.asm.Vu).apply(null,arguments)},lx=b._emscripten_bind_btKinematicCharacterController_onGround_0=function(){return(lx=b._emscripten_bind_btKinematicCharacterController_onGround_0=b.asm.Wu).apply(null,arguments)},mx=b._emscripten_bind_btKinematicCharacterController_setUpInterpolate_1=function(){return(mx= +b._emscripten_bind_btKinematicCharacterController_setUpInterpolate_1=b.asm.Xu).apply(null,arguments)},nx=b._emscripten_bind_btKinematicCharacterController_updateAction_2=function(){return(nx=b._emscripten_bind_btKinematicCharacterController_updateAction_2=b.asm.Yu).apply(null,arguments)},ox=b._emscripten_bind_btKinematicCharacterController___destroy___0=function(){return(ox=b._emscripten_bind_btKinematicCharacterController___destroy___0=b.asm.Zu).apply(null,arguments)},px=b._emscripten_bind_btRaycastVehicle_btRaycastVehicle_3= +function(){return(px=b._emscripten_bind_btRaycastVehicle_btRaycastVehicle_3=b.asm._u).apply(null,arguments)},qx=b._emscripten_bind_btRaycastVehicle_applyEngineForce_2=function(){return(qx=b._emscripten_bind_btRaycastVehicle_applyEngineForce_2=b.asm.$u).apply(null,arguments)},rx=b._emscripten_bind_btRaycastVehicle_setSteeringValue_2=function(){return(rx=b._emscripten_bind_btRaycastVehicle_setSteeringValue_2=b.asm.av).apply(null,arguments)},sx=b._emscripten_bind_btRaycastVehicle_getWheelTransformWS_1= +function(){return(sx=b._emscripten_bind_btRaycastVehicle_getWheelTransformWS_1=b.asm.bv).apply(null,arguments)},tx=b._emscripten_bind_btRaycastVehicle_updateWheelTransform_2=function(){return(tx=b._emscripten_bind_btRaycastVehicle_updateWheelTransform_2=b.asm.cv).apply(null,arguments)},ux=b._emscripten_bind_btRaycastVehicle_addWheel_7=function(){return(ux=b._emscripten_bind_btRaycastVehicle_addWheel_7=b.asm.dv).apply(null,arguments)},vx=b._emscripten_bind_btRaycastVehicle_getNumWheels_0=function(){return(vx= +b._emscripten_bind_btRaycastVehicle_getNumWheels_0=b.asm.ev).apply(null,arguments)},wx=b._emscripten_bind_btRaycastVehicle_getRigidBody_0=function(){return(wx=b._emscripten_bind_btRaycastVehicle_getRigidBody_0=b.asm.fv).apply(null,arguments)},xx=b._emscripten_bind_btRaycastVehicle_getWheelInfo_1=function(){return(xx=b._emscripten_bind_btRaycastVehicle_getWheelInfo_1=b.asm.gv).apply(null,arguments)},yx=b._emscripten_bind_btRaycastVehicle_setBrake_2=function(){return(yx=b._emscripten_bind_btRaycastVehicle_setBrake_2= +b.asm.hv).apply(null,arguments)},zx=b._emscripten_bind_btRaycastVehicle_setCoordinateSystem_3=function(){return(zx=b._emscripten_bind_btRaycastVehicle_setCoordinateSystem_3=b.asm.iv).apply(null,arguments)},Ax=b._emscripten_bind_btRaycastVehicle_getCurrentSpeedKmHour_0=function(){return(Ax=b._emscripten_bind_btRaycastVehicle_getCurrentSpeedKmHour_0=b.asm.jv).apply(null,arguments)},Bx=b._emscripten_bind_btRaycastVehicle_getChassisWorldTransform_0=function(){return(Bx=b._emscripten_bind_btRaycastVehicle_getChassisWorldTransform_0= +b.asm.kv).apply(null,arguments)},Cx=b._emscripten_bind_btRaycastVehicle_rayCast_1=function(){return(Cx=b._emscripten_bind_btRaycastVehicle_rayCast_1=b.asm.lv).apply(null,arguments)},Dx=b._emscripten_bind_btRaycastVehicle_updateVehicle_1=function(){return(Dx=b._emscripten_bind_btRaycastVehicle_updateVehicle_1=b.asm.mv).apply(null,arguments)},Ex=b._emscripten_bind_btRaycastVehicle_resetSuspension_0=function(){return(Ex=b._emscripten_bind_btRaycastVehicle_resetSuspension_0=b.asm.nv).apply(null,arguments)}, +Fx=b._emscripten_bind_btRaycastVehicle_getSteeringValue_1=function(){return(Fx=b._emscripten_bind_btRaycastVehicle_getSteeringValue_1=b.asm.ov).apply(null,arguments)},Gx=b._emscripten_bind_btRaycastVehicle_updateWheelTransformsWS_1=function(){return(Gx=b._emscripten_bind_btRaycastVehicle_updateWheelTransformsWS_1=b.asm.pv).apply(null,arguments)},Hx=b._emscripten_bind_btRaycastVehicle_updateWheelTransformsWS_2=function(){return(Hx=b._emscripten_bind_btRaycastVehicle_updateWheelTransformsWS_2=b.asm.qv).apply(null, +arguments)},Ix=b._emscripten_bind_btRaycastVehicle_setPitchControl_1=function(){return(Ix=b._emscripten_bind_btRaycastVehicle_setPitchControl_1=b.asm.rv).apply(null,arguments)},Jx=b._emscripten_bind_btRaycastVehicle_updateSuspension_1=function(){return(Jx=b._emscripten_bind_btRaycastVehicle_updateSuspension_1=b.asm.sv).apply(null,arguments)},Kx=b._emscripten_bind_btRaycastVehicle_updateFriction_1=function(){return(Kx=b._emscripten_bind_btRaycastVehicle_updateFriction_1=b.asm.tv).apply(null,arguments)}, +Lx=b._emscripten_bind_btRaycastVehicle_getRightAxis_0=function(){return(Lx=b._emscripten_bind_btRaycastVehicle_getRightAxis_0=b.asm.uv).apply(null,arguments)},Mx=b._emscripten_bind_btRaycastVehicle_getUpAxis_0=function(){return(Mx=b._emscripten_bind_btRaycastVehicle_getUpAxis_0=b.asm.vv).apply(null,arguments)},Nx=b._emscripten_bind_btRaycastVehicle_getForwardAxis_0=function(){return(Nx=b._emscripten_bind_btRaycastVehicle_getForwardAxis_0=b.asm.wv).apply(null,arguments)},Ox=b._emscripten_bind_btRaycastVehicle_getForwardVector_0= +function(){return(Ox=b._emscripten_bind_btRaycastVehicle_getForwardVector_0=b.asm.xv).apply(null,arguments)},Px=b._emscripten_bind_btRaycastVehicle_getUserConstraintType_0=function(){return(Px=b._emscripten_bind_btRaycastVehicle_getUserConstraintType_0=b.asm.yv).apply(null,arguments)},Qx=b._emscripten_bind_btRaycastVehicle_setUserConstraintType_1=function(){return(Qx=b._emscripten_bind_btRaycastVehicle_setUserConstraintType_1=b.asm.zv).apply(null,arguments)},Rx=b._emscripten_bind_btRaycastVehicle_setUserConstraintId_1= +function(){return(Rx=b._emscripten_bind_btRaycastVehicle_setUserConstraintId_1=b.asm.Av).apply(null,arguments)},Sx=b._emscripten_bind_btRaycastVehicle_getUserConstraintId_0=function(){return(Sx=b._emscripten_bind_btRaycastVehicle_getUserConstraintId_0=b.asm.Bv).apply(null,arguments)},Tx=b._emscripten_bind_btRaycastVehicle_updateAction_2=function(){return(Tx=b._emscripten_bind_btRaycastVehicle_updateAction_2=b.asm.Cv).apply(null,arguments)},Ux=b._emscripten_bind_btRaycastVehicle___destroy___0=function(){return(Ux= +b._emscripten_bind_btRaycastVehicle___destroy___0=b.asm.Dv).apply(null,arguments)},Vx=b._emscripten_bind_btPairCachingGhostObject_btPairCachingGhostObject_0=function(){return(Vx=b._emscripten_bind_btPairCachingGhostObject_btPairCachingGhostObject_0=b.asm.Ev).apply(null,arguments)},Wx=b._emscripten_bind_btPairCachingGhostObject_setAnisotropicFriction_2=function(){return(Wx=b._emscripten_bind_btPairCachingGhostObject_setAnisotropicFriction_2=b.asm.Fv).apply(null,arguments)},Xx=b._emscripten_bind_btPairCachingGhostObject_getCollisionShape_0= +function(){return(Xx=b._emscripten_bind_btPairCachingGhostObject_getCollisionShape_0=b.asm.Gv).apply(null,arguments)},Yx=b._emscripten_bind_btPairCachingGhostObject_setContactProcessingThreshold_1=function(){return(Yx=b._emscripten_bind_btPairCachingGhostObject_setContactProcessingThreshold_1=b.asm.Hv).apply(null,arguments)},Zx=b._emscripten_bind_btPairCachingGhostObject_setActivationState_1=function(){return(Zx=b._emscripten_bind_btPairCachingGhostObject_setActivationState_1=b.asm.Iv).apply(null, +arguments)},$x=b._emscripten_bind_btPairCachingGhostObject_forceActivationState_1=function(){return($x=b._emscripten_bind_btPairCachingGhostObject_forceActivationState_1=b.asm.Jv).apply(null,arguments)},ay=b._emscripten_bind_btPairCachingGhostObject_activate_0=function(){return(ay=b._emscripten_bind_btPairCachingGhostObject_activate_0=b.asm.Kv).apply(null,arguments)},by=b._emscripten_bind_btPairCachingGhostObject_activate_1=function(){return(by=b._emscripten_bind_btPairCachingGhostObject_activate_1= +b.asm.Lv).apply(null,arguments)},cy=b._emscripten_bind_btPairCachingGhostObject_isActive_0=function(){return(cy=b._emscripten_bind_btPairCachingGhostObject_isActive_0=b.asm.Mv).apply(null,arguments)},dy=b._emscripten_bind_btPairCachingGhostObject_isKinematicObject_0=function(){return(dy=b._emscripten_bind_btPairCachingGhostObject_isKinematicObject_0=b.asm.Nv).apply(null,arguments)},ey=b._emscripten_bind_btPairCachingGhostObject_isStaticObject_0=function(){return(ey=b._emscripten_bind_btPairCachingGhostObject_isStaticObject_0= +b.asm.Ov).apply(null,arguments)},fy=b._emscripten_bind_btPairCachingGhostObject_isStaticOrKinematicObject_0=function(){return(fy=b._emscripten_bind_btPairCachingGhostObject_isStaticOrKinematicObject_0=b.asm.Pv).apply(null,arguments)},gy=b._emscripten_bind_btPairCachingGhostObject_getRestitution_0=function(){return(gy=b._emscripten_bind_btPairCachingGhostObject_getRestitution_0=b.asm.Qv).apply(null,arguments)},hy=b._emscripten_bind_btPairCachingGhostObject_getFriction_0=function(){return(hy=b._emscripten_bind_btPairCachingGhostObject_getFriction_0= +b.asm.Rv).apply(null,arguments)},iy=b._emscripten_bind_btPairCachingGhostObject_getRollingFriction_0=function(){return(iy=b._emscripten_bind_btPairCachingGhostObject_getRollingFriction_0=b.asm.Sv).apply(null,arguments)},jy=b._emscripten_bind_btPairCachingGhostObject_setRestitution_1=function(){return(jy=b._emscripten_bind_btPairCachingGhostObject_setRestitution_1=b.asm.Tv).apply(null,arguments)},ky=b._emscripten_bind_btPairCachingGhostObject_setFriction_1=function(){return(ky=b._emscripten_bind_btPairCachingGhostObject_setFriction_1= +b.asm.Uv).apply(null,arguments)},ly=b._emscripten_bind_btPairCachingGhostObject_setRollingFriction_1=function(){return(ly=b._emscripten_bind_btPairCachingGhostObject_setRollingFriction_1=b.asm.Vv).apply(null,arguments)},my=b._emscripten_bind_btPairCachingGhostObject_getWorldTransform_0=function(){return(my=b._emscripten_bind_btPairCachingGhostObject_getWorldTransform_0=b.asm.Wv).apply(null,arguments)},ny=b._emscripten_bind_btPairCachingGhostObject_getCollisionFlags_0=function(){return(ny=b._emscripten_bind_btPairCachingGhostObject_getCollisionFlags_0= +b.asm.Xv).apply(null,arguments)},oy=b._emscripten_bind_btPairCachingGhostObject_setCollisionFlags_1=function(){return(oy=b._emscripten_bind_btPairCachingGhostObject_setCollisionFlags_1=b.asm.Yv).apply(null,arguments)},py=b._emscripten_bind_btPairCachingGhostObject_setWorldTransform_1=function(){return(py=b._emscripten_bind_btPairCachingGhostObject_setWorldTransform_1=b.asm.Zv).apply(null,arguments)},qy=b._emscripten_bind_btPairCachingGhostObject_setCollisionShape_1=function(){return(qy=b._emscripten_bind_btPairCachingGhostObject_setCollisionShape_1= +b.asm._v).apply(null,arguments)},ry=b._emscripten_bind_btPairCachingGhostObject_setCcdMotionThreshold_1=function(){return(ry=b._emscripten_bind_btPairCachingGhostObject_setCcdMotionThreshold_1=b.asm.$v).apply(null,arguments)},sy=b._emscripten_bind_btPairCachingGhostObject_setCcdSweptSphereRadius_1=function(){return(sy=b._emscripten_bind_btPairCachingGhostObject_setCcdSweptSphereRadius_1=b.asm.aw).apply(null,arguments)},ty=b._emscripten_bind_btPairCachingGhostObject_getUserIndex_0=function(){return(ty= +b._emscripten_bind_btPairCachingGhostObject_getUserIndex_0=b.asm.bw).apply(null,arguments)},uy=b._emscripten_bind_btPairCachingGhostObject_setUserIndex_1=function(){return(uy=b._emscripten_bind_btPairCachingGhostObject_setUserIndex_1=b.asm.cw).apply(null,arguments)},vy=b._emscripten_bind_btPairCachingGhostObject_getUserPointer_0=function(){return(vy=b._emscripten_bind_btPairCachingGhostObject_getUserPointer_0=b.asm.dw).apply(null,arguments)},wy=b._emscripten_bind_btPairCachingGhostObject_setUserPointer_1= +function(){return(wy=b._emscripten_bind_btPairCachingGhostObject_setUserPointer_1=b.asm.ew).apply(null,arguments)},xy=b._emscripten_bind_btPairCachingGhostObject_getBroadphaseHandle_0=function(){return(xy=b._emscripten_bind_btPairCachingGhostObject_getBroadphaseHandle_0=b.asm.fw).apply(null,arguments)},yy=b._emscripten_bind_btPairCachingGhostObject_getNumOverlappingObjects_0=function(){return(yy=b._emscripten_bind_btPairCachingGhostObject_getNumOverlappingObjects_0=b.asm.gw).apply(null,arguments)}, +zy=b._emscripten_bind_btPairCachingGhostObject_getOverlappingObject_1=function(){return(zy=b._emscripten_bind_btPairCachingGhostObject_getOverlappingObject_1=b.asm.hw).apply(null,arguments)},Ay=b._emscripten_bind_btPairCachingGhostObject___destroy___0=function(){return(Ay=b._emscripten_bind_btPairCachingGhostObject___destroy___0=b.asm.iw).apply(null,arguments)},By=b._emscripten_bind_btGhostPairCallback_btGhostPairCallback_0=function(){return(By=b._emscripten_bind_btGhostPairCallback_btGhostPairCallback_0= +b.asm.jw).apply(null,arguments)},Cy=b._emscripten_bind_btGhostPairCallback___destroy___0=function(){return(Cy=b._emscripten_bind_btGhostPairCallback___destroy___0=b.asm.kw).apply(null,arguments)},Dy=b._emscripten_bind_btSoftBodyWorldInfo_btSoftBodyWorldInfo_0=function(){return(Dy=b._emscripten_bind_btSoftBodyWorldInfo_btSoftBodyWorldInfo_0=b.asm.lw).apply(null,arguments)},Ey=b._emscripten_bind_btSoftBodyWorldInfo_get_air_density_0=function(){return(Ey=b._emscripten_bind_btSoftBodyWorldInfo_get_air_density_0= +b.asm.mw).apply(null,arguments)},Fy=b._emscripten_bind_btSoftBodyWorldInfo_set_air_density_1=function(){return(Fy=b._emscripten_bind_btSoftBodyWorldInfo_set_air_density_1=b.asm.nw).apply(null,arguments)},Gy=b._emscripten_bind_btSoftBodyWorldInfo_get_water_density_0=function(){return(Gy=b._emscripten_bind_btSoftBodyWorldInfo_get_water_density_0=b.asm.ow).apply(null,arguments)},Hy=b._emscripten_bind_btSoftBodyWorldInfo_set_water_density_1=function(){return(Hy=b._emscripten_bind_btSoftBodyWorldInfo_set_water_density_1= +b.asm.pw).apply(null,arguments)},Iy=b._emscripten_bind_btSoftBodyWorldInfo_get_water_offset_0=function(){return(Iy=b._emscripten_bind_btSoftBodyWorldInfo_get_water_offset_0=b.asm.qw).apply(null,arguments)},Jy=b._emscripten_bind_btSoftBodyWorldInfo_set_water_offset_1=function(){return(Jy=b._emscripten_bind_btSoftBodyWorldInfo_set_water_offset_1=b.asm.rw).apply(null,arguments)},Ky=b._emscripten_bind_btSoftBodyWorldInfo_get_m_maxDisplacement_0=function(){return(Ky=b._emscripten_bind_btSoftBodyWorldInfo_get_m_maxDisplacement_0= +b.asm.sw).apply(null,arguments)},Ly=b._emscripten_bind_btSoftBodyWorldInfo_set_m_maxDisplacement_1=function(){return(Ly=b._emscripten_bind_btSoftBodyWorldInfo_set_m_maxDisplacement_1=b.asm.tw).apply(null,arguments)},My=b._emscripten_bind_btSoftBodyWorldInfo_get_water_normal_0=function(){return(My=b._emscripten_bind_btSoftBodyWorldInfo_get_water_normal_0=b.asm.uw).apply(null,arguments)},Ny=b._emscripten_bind_btSoftBodyWorldInfo_set_water_normal_1=function(){return(Ny=b._emscripten_bind_btSoftBodyWorldInfo_set_water_normal_1= +b.asm.vw).apply(null,arguments)},Oy=b._emscripten_bind_btSoftBodyWorldInfo_get_m_broadphase_0=function(){return(Oy=b._emscripten_bind_btSoftBodyWorldInfo_get_m_broadphase_0=b.asm.ww).apply(null,arguments)},Py=b._emscripten_bind_btSoftBodyWorldInfo_set_m_broadphase_1=function(){return(Py=b._emscripten_bind_btSoftBodyWorldInfo_set_m_broadphase_1=b.asm.xw).apply(null,arguments)},Qy=b._emscripten_bind_btSoftBodyWorldInfo_get_m_dispatcher_0=function(){return(Qy=b._emscripten_bind_btSoftBodyWorldInfo_get_m_dispatcher_0= +b.asm.yw).apply(null,arguments)},Ry=b._emscripten_bind_btSoftBodyWorldInfo_set_m_dispatcher_1=function(){return(Ry=b._emscripten_bind_btSoftBodyWorldInfo_set_m_dispatcher_1=b.asm.zw).apply(null,arguments)},Sy=b._emscripten_bind_btSoftBodyWorldInfo_get_m_gravity_0=function(){return(Sy=b._emscripten_bind_btSoftBodyWorldInfo_get_m_gravity_0=b.asm.Aw).apply(null,arguments)},Ty=b._emscripten_bind_btSoftBodyWorldInfo_set_m_gravity_1=function(){return(Ty=b._emscripten_bind_btSoftBodyWorldInfo_set_m_gravity_1= +b.asm.Bw).apply(null,arguments)},Uy=b._emscripten_bind_btSoftBodyWorldInfo___destroy___0=function(){return(Uy=b._emscripten_bind_btSoftBodyWorldInfo___destroy___0=b.asm.Cw).apply(null,arguments)},Vy=b._emscripten_bind_Face_get_m_n_1=function(){return(Vy=b._emscripten_bind_Face_get_m_n_1=b.asm.Dw).apply(null,arguments)},Wy=b._emscripten_bind_Face_set_m_n_2=function(){return(Wy=b._emscripten_bind_Face_set_m_n_2=b.asm.Ew).apply(null,arguments)},Xy=b._emscripten_bind_Face_get_m_normal_0=function(){return(Xy= +b._emscripten_bind_Face_get_m_normal_0=b.asm.Fw).apply(null,arguments)},Yy=b._emscripten_bind_Face_set_m_normal_1=function(){return(Yy=b._emscripten_bind_Face_set_m_normal_1=b.asm.Gw).apply(null,arguments)},Zy=b._emscripten_bind_Face_get_m_ra_0=function(){return(Zy=b._emscripten_bind_Face_get_m_ra_0=b.asm.Hw).apply(null,arguments)},$y=b._emscripten_bind_Face_set_m_ra_1=function(){return($y=b._emscripten_bind_Face_set_m_ra_1=b.asm.Iw).apply(null,arguments)},az=b._emscripten_bind_Face___destroy___0= +function(){return(az=b._emscripten_bind_Face___destroy___0=b.asm.Jw).apply(null,arguments)},bz=b._emscripten_bind_tFaceArray_size_0=function(){return(bz=b._emscripten_bind_tFaceArray_size_0=b.asm.Kw).apply(null,arguments)},cz=b._emscripten_bind_tFaceArray_at_1=function(){return(cz=b._emscripten_bind_tFaceArray_at_1=b.asm.Lw).apply(null,arguments)},dz=b._emscripten_bind_tFaceArray___destroy___0=function(){return(dz=b._emscripten_bind_tFaceArray___destroy___0=b.asm.Mw).apply(null,arguments)},ez=b._emscripten_bind_Node_get_m_x_0= +function(){return(ez=b._emscripten_bind_Node_get_m_x_0=b.asm.Nw).apply(null,arguments)},fz=b._emscripten_bind_Node_set_m_x_1=function(){return(fz=b._emscripten_bind_Node_set_m_x_1=b.asm.Ow).apply(null,arguments)},gz=b._emscripten_bind_Node_get_m_q_0=function(){return(gz=b._emscripten_bind_Node_get_m_q_0=b.asm.Pw).apply(null,arguments)},hz=b._emscripten_bind_Node_set_m_q_1=function(){return(hz=b._emscripten_bind_Node_set_m_q_1=b.asm.Qw).apply(null,arguments)},iz=b._emscripten_bind_Node_get_m_v_0=function(){return(iz= +b._emscripten_bind_Node_get_m_v_0=b.asm.Rw).apply(null,arguments)},jz=b._emscripten_bind_Node_set_m_v_1=function(){return(jz=b._emscripten_bind_Node_set_m_v_1=b.asm.Sw).apply(null,arguments)},kz=b._emscripten_bind_Node_get_m_f_0=function(){return(kz=b._emscripten_bind_Node_get_m_f_0=b.asm.Tw).apply(null,arguments)},lz=b._emscripten_bind_Node_set_m_f_1=function(){return(lz=b._emscripten_bind_Node_set_m_f_1=b.asm.Uw).apply(null,arguments)},mz=b._emscripten_bind_Node_get_m_n_0=function(){return(mz=b._emscripten_bind_Node_get_m_n_0= +b.asm.Vw).apply(null,arguments)},nz=b._emscripten_bind_Node_set_m_n_1=function(){return(nz=b._emscripten_bind_Node_set_m_n_1=b.asm.Ww).apply(null,arguments)},oz=b._emscripten_bind_Node_get_m_im_0=function(){return(oz=b._emscripten_bind_Node_get_m_im_0=b.asm.Xw).apply(null,arguments)},pz=b._emscripten_bind_Node_set_m_im_1=function(){return(pz=b._emscripten_bind_Node_set_m_im_1=b.asm.Yw).apply(null,arguments)},qz=b._emscripten_bind_Node_get_m_area_0=function(){return(qz=b._emscripten_bind_Node_get_m_area_0= +b.asm.Zw).apply(null,arguments)},rz=b._emscripten_bind_Node_set_m_area_1=function(){return(rz=b._emscripten_bind_Node_set_m_area_1=b.asm._w).apply(null,arguments)},sz=b._emscripten_bind_Node___destroy___0=function(){return(sz=b._emscripten_bind_Node___destroy___0=b.asm.$w).apply(null,arguments)},tz=b._emscripten_bind_tNodeArray_size_0=function(){return(tz=b._emscripten_bind_tNodeArray_size_0=b.asm.ax).apply(null,arguments)},uz=b._emscripten_bind_tNodeArray_at_1=function(){return(uz=b._emscripten_bind_tNodeArray_at_1= +b.asm.bx).apply(null,arguments)},vz=b._emscripten_bind_tNodeArray___destroy___0=function(){return(vz=b._emscripten_bind_tNodeArray___destroy___0=b.asm.cx).apply(null,arguments)},wz=b._emscripten_bind_Material_get_m_kLST_0=function(){return(wz=b._emscripten_bind_Material_get_m_kLST_0=b.asm.dx).apply(null,arguments)},xz=b._emscripten_bind_Material_set_m_kLST_1=function(){return(xz=b._emscripten_bind_Material_set_m_kLST_1=b.asm.ex).apply(null,arguments)},yz=b._emscripten_bind_Material_get_m_kAST_0=function(){return(yz= +b._emscripten_bind_Material_get_m_kAST_0=b.asm.fx).apply(null,arguments)},zz=b._emscripten_bind_Material_set_m_kAST_1=function(){return(zz=b._emscripten_bind_Material_set_m_kAST_1=b.asm.gx).apply(null,arguments)},Az=b._emscripten_bind_Material_get_m_kVST_0=function(){return(Az=b._emscripten_bind_Material_get_m_kVST_0=b.asm.hx).apply(null,arguments)},Bz=b._emscripten_bind_Material_set_m_kVST_1=function(){return(Bz=b._emscripten_bind_Material_set_m_kVST_1=b.asm.ix).apply(null,arguments)},Cz=b._emscripten_bind_Material_get_m_flags_0= +function(){return(Cz=b._emscripten_bind_Material_get_m_flags_0=b.asm.jx).apply(null,arguments)},Dz=b._emscripten_bind_Material_set_m_flags_1=function(){return(Dz=b._emscripten_bind_Material_set_m_flags_1=b.asm.kx).apply(null,arguments)},Ez=b._emscripten_bind_Material___destroy___0=function(){return(Ez=b._emscripten_bind_Material___destroy___0=b.asm.lx).apply(null,arguments)},Fz=b._emscripten_bind_tMaterialArray_size_0=function(){return(Fz=b._emscripten_bind_tMaterialArray_size_0=b.asm.mx).apply(null, +arguments)},Gz=b._emscripten_bind_tMaterialArray_at_1=function(){return(Gz=b._emscripten_bind_tMaterialArray_at_1=b.asm.nx).apply(null,arguments)},Hz=b._emscripten_bind_tMaterialArray___destroy___0=function(){return(Hz=b._emscripten_bind_tMaterialArray___destroy___0=b.asm.ox).apply(null,arguments)},Iz=b._emscripten_bind_Anchor_get_m_node_0=function(){return(Iz=b._emscripten_bind_Anchor_get_m_node_0=b.asm.px).apply(null,arguments)},Jz=b._emscripten_bind_Anchor_set_m_node_1=function(){return(Jz=b._emscripten_bind_Anchor_set_m_node_1= +b.asm.qx).apply(null,arguments)},Kz=b._emscripten_bind_Anchor_get_m_local_0=function(){return(Kz=b._emscripten_bind_Anchor_get_m_local_0=b.asm.rx).apply(null,arguments)},Lz=b._emscripten_bind_Anchor_set_m_local_1=function(){return(Lz=b._emscripten_bind_Anchor_set_m_local_1=b.asm.sx).apply(null,arguments)},Mz=b._emscripten_bind_Anchor_get_m_body_0=function(){return(Mz=b._emscripten_bind_Anchor_get_m_body_0=b.asm.tx).apply(null,arguments)},Nz=b._emscripten_bind_Anchor_set_m_body_1=function(){return(Nz= +b._emscripten_bind_Anchor_set_m_body_1=b.asm.ux).apply(null,arguments)},Oz=b._emscripten_bind_Anchor_get_m_influence_0=function(){return(Oz=b._emscripten_bind_Anchor_get_m_influence_0=b.asm.vx).apply(null,arguments)},Pz=b._emscripten_bind_Anchor_set_m_influence_1=function(){return(Pz=b._emscripten_bind_Anchor_set_m_influence_1=b.asm.wx).apply(null,arguments)},Qz=b._emscripten_bind_Anchor_get_m_c0_0=function(){return(Qz=b._emscripten_bind_Anchor_get_m_c0_0=b.asm.xx).apply(null,arguments)},Rz=b._emscripten_bind_Anchor_set_m_c0_1= +function(){return(Rz=b._emscripten_bind_Anchor_set_m_c0_1=b.asm.yx).apply(null,arguments)},Sz=b._emscripten_bind_Anchor_get_m_c1_0=function(){return(Sz=b._emscripten_bind_Anchor_get_m_c1_0=b.asm.zx).apply(null,arguments)},Tz=b._emscripten_bind_Anchor_set_m_c1_1=function(){return(Tz=b._emscripten_bind_Anchor_set_m_c1_1=b.asm.Ax).apply(null,arguments)},Uz=b._emscripten_bind_Anchor_get_m_c2_0=function(){return(Uz=b._emscripten_bind_Anchor_get_m_c2_0=b.asm.Bx).apply(null,arguments)},Vz=b._emscripten_bind_Anchor_set_m_c2_1= +function(){return(Vz=b._emscripten_bind_Anchor_set_m_c2_1=b.asm.Cx).apply(null,arguments)},Wz=b._emscripten_bind_Anchor___destroy___0=function(){return(Wz=b._emscripten_bind_Anchor___destroy___0=b.asm.Dx).apply(null,arguments)},Xz=b._emscripten_bind_tAnchorArray_size_0=function(){return(Xz=b._emscripten_bind_tAnchorArray_size_0=b.asm.Ex).apply(null,arguments)},Yz=b._emscripten_bind_tAnchorArray_at_1=function(){return(Yz=b._emscripten_bind_tAnchorArray_at_1=b.asm.Fx).apply(null,arguments)},Zz=b._emscripten_bind_tAnchorArray_clear_0= +function(){return(Zz=b._emscripten_bind_tAnchorArray_clear_0=b.asm.Gx).apply(null,arguments)},$z=b._emscripten_bind_tAnchorArray_push_back_1=function(){return($z=b._emscripten_bind_tAnchorArray_push_back_1=b.asm.Hx).apply(null,arguments)},aA=b._emscripten_bind_tAnchorArray_pop_back_0=function(){return(aA=b._emscripten_bind_tAnchorArray_pop_back_0=b.asm.Ix).apply(null,arguments)},bA=b._emscripten_bind_tAnchorArray___destroy___0=function(){return(bA=b._emscripten_bind_tAnchorArray___destroy___0=b.asm.Jx).apply(null, +arguments)},cA=b._emscripten_bind_Config_get_kVCF_0=function(){return(cA=b._emscripten_bind_Config_get_kVCF_0=b.asm.Kx).apply(null,arguments)},dA=b._emscripten_bind_Config_set_kVCF_1=function(){return(dA=b._emscripten_bind_Config_set_kVCF_1=b.asm.Lx).apply(null,arguments)},eA=b._emscripten_bind_Config_get_kDP_0=function(){return(eA=b._emscripten_bind_Config_get_kDP_0=b.asm.Mx).apply(null,arguments)},fA=b._emscripten_bind_Config_set_kDP_1=function(){return(fA=b._emscripten_bind_Config_set_kDP_1=b.asm.Nx).apply(null, +arguments)},gA=b._emscripten_bind_Config_get_kDG_0=function(){return(gA=b._emscripten_bind_Config_get_kDG_0=b.asm.Ox).apply(null,arguments)},hA=b._emscripten_bind_Config_set_kDG_1=function(){return(hA=b._emscripten_bind_Config_set_kDG_1=b.asm.Px).apply(null,arguments)},iA=b._emscripten_bind_Config_get_kLF_0=function(){return(iA=b._emscripten_bind_Config_get_kLF_0=b.asm.Qx).apply(null,arguments)},jA=b._emscripten_bind_Config_set_kLF_1=function(){return(jA=b._emscripten_bind_Config_set_kLF_1=b.asm.Rx).apply(null, +arguments)},kA=b._emscripten_bind_Config_get_kPR_0=function(){return(kA=b._emscripten_bind_Config_get_kPR_0=b.asm.Sx).apply(null,arguments)},lA=b._emscripten_bind_Config_set_kPR_1=function(){return(lA=b._emscripten_bind_Config_set_kPR_1=b.asm.Tx).apply(null,arguments)},mA=b._emscripten_bind_Config_get_kVC_0=function(){return(mA=b._emscripten_bind_Config_get_kVC_0=b.asm.Ux).apply(null,arguments)},nA=b._emscripten_bind_Config_set_kVC_1=function(){return(nA=b._emscripten_bind_Config_set_kVC_1=b.asm.Vx).apply(null, +arguments)},oA=b._emscripten_bind_Config_get_kDF_0=function(){return(oA=b._emscripten_bind_Config_get_kDF_0=b.asm.Wx).apply(null,arguments)},pA=b._emscripten_bind_Config_set_kDF_1=function(){return(pA=b._emscripten_bind_Config_set_kDF_1=b.asm.Xx).apply(null,arguments)},qA=b._emscripten_bind_Config_get_kMT_0=function(){return(qA=b._emscripten_bind_Config_get_kMT_0=b.asm.Yx).apply(null,arguments)},rA=b._emscripten_bind_Config_set_kMT_1=function(){return(rA=b._emscripten_bind_Config_set_kMT_1=b.asm.Zx).apply(null, +arguments)},sA=b._emscripten_bind_Config_get_kCHR_0=function(){return(sA=b._emscripten_bind_Config_get_kCHR_0=b.asm._x).apply(null,arguments)},tA=b._emscripten_bind_Config_set_kCHR_1=function(){return(tA=b._emscripten_bind_Config_set_kCHR_1=b.asm.$x).apply(null,arguments)},uA=b._emscripten_bind_Config_get_kKHR_0=function(){return(uA=b._emscripten_bind_Config_get_kKHR_0=b.asm.ay).apply(null,arguments)},vA=b._emscripten_bind_Config_set_kKHR_1=function(){return(vA=b._emscripten_bind_Config_set_kKHR_1= +b.asm.by).apply(null,arguments)},wA=b._emscripten_bind_Config_get_kSHR_0=function(){return(wA=b._emscripten_bind_Config_get_kSHR_0=b.asm.cy).apply(null,arguments)},xA=b._emscripten_bind_Config_set_kSHR_1=function(){return(xA=b._emscripten_bind_Config_set_kSHR_1=b.asm.dy).apply(null,arguments)},yA=b._emscripten_bind_Config_get_kAHR_0=function(){return(yA=b._emscripten_bind_Config_get_kAHR_0=b.asm.ey).apply(null,arguments)},zA=b._emscripten_bind_Config_set_kAHR_1=function(){return(zA=b._emscripten_bind_Config_set_kAHR_1= +b.asm.fy).apply(null,arguments)},AA=b._emscripten_bind_Config_get_kSRHR_CL_0=function(){return(AA=b._emscripten_bind_Config_get_kSRHR_CL_0=b.asm.gy).apply(null,arguments)},BA=b._emscripten_bind_Config_set_kSRHR_CL_1=function(){return(BA=b._emscripten_bind_Config_set_kSRHR_CL_1=b.asm.hy).apply(null,arguments)},CA=b._emscripten_bind_Config_get_kSKHR_CL_0=function(){return(CA=b._emscripten_bind_Config_get_kSKHR_CL_0=b.asm.iy).apply(null,arguments)},DA=b._emscripten_bind_Config_set_kSKHR_CL_1=function(){return(DA= +b._emscripten_bind_Config_set_kSKHR_CL_1=b.asm.jy).apply(null,arguments)},EA=b._emscripten_bind_Config_get_kSSHR_CL_0=function(){return(EA=b._emscripten_bind_Config_get_kSSHR_CL_0=b.asm.ky).apply(null,arguments)},FA=b._emscripten_bind_Config_set_kSSHR_CL_1=function(){return(FA=b._emscripten_bind_Config_set_kSSHR_CL_1=b.asm.ly).apply(null,arguments)},GA=b._emscripten_bind_Config_get_kSR_SPLT_CL_0=function(){return(GA=b._emscripten_bind_Config_get_kSR_SPLT_CL_0=b.asm.my).apply(null,arguments)},HA=b._emscripten_bind_Config_set_kSR_SPLT_CL_1= +function(){return(HA=b._emscripten_bind_Config_set_kSR_SPLT_CL_1=b.asm.ny).apply(null,arguments)},IA=b._emscripten_bind_Config_get_kSK_SPLT_CL_0=function(){return(IA=b._emscripten_bind_Config_get_kSK_SPLT_CL_0=b.asm.oy).apply(null,arguments)},JA=b._emscripten_bind_Config_set_kSK_SPLT_CL_1=function(){return(JA=b._emscripten_bind_Config_set_kSK_SPLT_CL_1=b.asm.py).apply(null,arguments)},KA=b._emscripten_bind_Config_get_kSS_SPLT_CL_0=function(){return(KA=b._emscripten_bind_Config_get_kSS_SPLT_CL_0=b.asm.qy).apply(null, +arguments)},LA=b._emscripten_bind_Config_set_kSS_SPLT_CL_1=function(){return(LA=b._emscripten_bind_Config_set_kSS_SPLT_CL_1=b.asm.ry).apply(null,arguments)},MA=b._emscripten_bind_Config_get_maxvolume_0=function(){return(MA=b._emscripten_bind_Config_get_maxvolume_0=b.asm.sy).apply(null,arguments)},NA=b._emscripten_bind_Config_set_maxvolume_1=function(){return(NA=b._emscripten_bind_Config_set_maxvolume_1=b.asm.ty).apply(null,arguments)},OA=b._emscripten_bind_Config_get_timescale_0=function(){return(OA= +b._emscripten_bind_Config_get_timescale_0=b.asm.uy).apply(null,arguments)},PA=b._emscripten_bind_Config_set_timescale_1=function(){return(PA=b._emscripten_bind_Config_set_timescale_1=b.asm.vy).apply(null,arguments)},QA=b._emscripten_bind_Config_get_viterations_0=function(){return(QA=b._emscripten_bind_Config_get_viterations_0=b.asm.wy).apply(null,arguments)},RA=b._emscripten_bind_Config_set_viterations_1=function(){return(RA=b._emscripten_bind_Config_set_viterations_1=b.asm.xy).apply(null,arguments)}, +SA=b._emscripten_bind_Config_get_piterations_0=function(){return(SA=b._emscripten_bind_Config_get_piterations_0=b.asm.yy).apply(null,arguments)},TA=b._emscripten_bind_Config_set_piterations_1=function(){return(TA=b._emscripten_bind_Config_set_piterations_1=b.asm.zy).apply(null,arguments)},UA=b._emscripten_bind_Config_get_diterations_0=function(){return(UA=b._emscripten_bind_Config_get_diterations_0=b.asm.Ay).apply(null,arguments)},VA=b._emscripten_bind_Config_set_diterations_1=function(){return(VA= +b._emscripten_bind_Config_set_diterations_1=b.asm.By).apply(null,arguments)},WA=b._emscripten_bind_Config_get_citerations_0=function(){return(WA=b._emscripten_bind_Config_get_citerations_0=b.asm.Cy).apply(null,arguments)},XA=b._emscripten_bind_Config_set_citerations_1=function(){return(XA=b._emscripten_bind_Config_set_citerations_1=b.asm.Dy).apply(null,arguments)},YA=b._emscripten_bind_Config_get_collisions_0=function(){return(YA=b._emscripten_bind_Config_get_collisions_0=b.asm.Ey).apply(null,arguments)}, +ZA=b._emscripten_bind_Config_set_collisions_1=function(){return(ZA=b._emscripten_bind_Config_set_collisions_1=b.asm.Fy).apply(null,arguments)},$A=b._emscripten_bind_Config___destroy___0=function(){return($A=b._emscripten_bind_Config___destroy___0=b.asm.Gy).apply(null,arguments)},aB=b._emscripten_bind_btSoftBody_btSoftBody_4=function(){return(aB=b._emscripten_bind_btSoftBody_btSoftBody_4=b.asm.Hy).apply(null,arguments)},bB=b._emscripten_bind_btSoftBody_checkLink_2=function(){return(bB=b._emscripten_bind_btSoftBody_checkLink_2= +b.asm.Iy).apply(null,arguments)},cB=b._emscripten_bind_btSoftBody_checkFace_3=function(){return(cB=b._emscripten_bind_btSoftBody_checkFace_3=b.asm.Jy).apply(null,arguments)},dB=b._emscripten_bind_btSoftBody_appendMaterial_0=function(){return(dB=b._emscripten_bind_btSoftBody_appendMaterial_0=b.asm.Ky).apply(null,arguments)},eB=b._emscripten_bind_btSoftBody_appendNode_2=function(){return(eB=b._emscripten_bind_btSoftBody_appendNode_2=b.asm.Ly).apply(null,arguments)},fB=b._emscripten_bind_btSoftBody_appendLink_4= +function(){return(fB=b._emscripten_bind_btSoftBody_appendLink_4=b.asm.My).apply(null,arguments)},gB=b._emscripten_bind_btSoftBody_appendFace_4=function(){return(gB=b._emscripten_bind_btSoftBody_appendFace_4=b.asm.Ny).apply(null,arguments)},hB=b._emscripten_bind_btSoftBody_appendTetra_5=function(){return(hB=b._emscripten_bind_btSoftBody_appendTetra_5=b.asm.Oy).apply(null,arguments)},iB=b._emscripten_bind_btSoftBody_appendAnchor_4=function(){return(iB=b._emscripten_bind_btSoftBody_appendAnchor_4=b.asm.Py).apply(null, +arguments)},jB=b._emscripten_bind_btSoftBody_addForce_1=function(){return(jB=b._emscripten_bind_btSoftBody_addForce_1=b.asm.Qy).apply(null,arguments)},kB=b._emscripten_bind_btSoftBody_addForce_2=function(){return(kB=b._emscripten_bind_btSoftBody_addForce_2=b.asm.Ry).apply(null,arguments)},lB=b._emscripten_bind_btSoftBody_addAeroForceToNode_2=function(){return(lB=b._emscripten_bind_btSoftBody_addAeroForceToNode_2=b.asm.Sy).apply(null,arguments)},mB=b._emscripten_bind_btSoftBody_getTotalMass_0=function(){return(mB= +b._emscripten_bind_btSoftBody_getTotalMass_0=b.asm.Ty).apply(null,arguments)},nB=b._emscripten_bind_btSoftBody_setTotalMass_2=function(){return(nB=b._emscripten_bind_btSoftBody_setTotalMass_2=b.asm.Uy).apply(null,arguments)},oB=b._emscripten_bind_btSoftBody_setMass_2=function(){return(oB=b._emscripten_bind_btSoftBody_setMass_2=b.asm.Vy).apply(null,arguments)},pB=b._emscripten_bind_btSoftBody_transform_1=function(){return(pB=b._emscripten_bind_btSoftBody_transform_1=b.asm.Wy).apply(null,arguments)}, +qB=b._emscripten_bind_btSoftBody_translate_1=function(){return(qB=b._emscripten_bind_btSoftBody_translate_1=b.asm.Xy).apply(null,arguments)},rB=b._emscripten_bind_btSoftBody_rotate_1=function(){return(rB=b._emscripten_bind_btSoftBody_rotate_1=b.asm.Yy).apply(null,arguments)},sB=b._emscripten_bind_btSoftBody_scale_1=function(){return(sB=b._emscripten_bind_btSoftBody_scale_1=b.asm.Zy).apply(null,arguments)},tB=b._emscripten_bind_btSoftBody_generateClusters_1=function(){return(tB=b._emscripten_bind_btSoftBody_generateClusters_1= +b.asm._y).apply(null,arguments)},uB=b._emscripten_bind_btSoftBody_generateClusters_2=function(){return(uB=b._emscripten_bind_btSoftBody_generateClusters_2=b.asm.$y).apply(null,arguments)},vB=b._emscripten_bind_btSoftBody_generateBendingConstraints_2=function(){return(vB=b._emscripten_bind_btSoftBody_generateBendingConstraints_2=b.asm.az).apply(null,arguments)},wB=b._emscripten_bind_btSoftBody_upcast_1=function(){return(wB=b._emscripten_bind_btSoftBody_upcast_1=b.asm.bz).apply(null,arguments)},xB= +b._emscripten_bind_btSoftBody_getRestLengthScale_0=function(){return(xB=b._emscripten_bind_btSoftBody_getRestLengthScale_0=b.asm.cz).apply(null,arguments)},yB=b._emscripten_bind_btSoftBody_setRestLengthScale_1=function(){return(yB=b._emscripten_bind_btSoftBody_setRestLengthScale_1=b.asm.dz).apply(null,arguments)},zB=b._emscripten_bind_btSoftBody_setAnisotropicFriction_2=function(){return(zB=b._emscripten_bind_btSoftBody_setAnisotropicFriction_2=b.asm.ez).apply(null,arguments)},AB=b._emscripten_bind_btSoftBody_getCollisionShape_0= +function(){return(AB=b._emscripten_bind_btSoftBody_getCollisionShape_0=b.asm.fz).apply(null,arguments)},BB=b._emscripten_bind_btSoftBody_setContactProcessingThreshold_1=function(){return(BB=b._emscripten_bind_btSoftBody_setContactProcessingThreshold_1=b.asm.gz).apply(null,arguments)},CB=b._emscripten_bind_btSoftBody_setActivationState_1=function(){return(CB=b._emscripten_bind_btSoftBody_setActivationState_1=b.asm.hz).apply(null,arguments)},DB=b._emscripten_bind_btSoftBody_forceActivationState_1=function(){return(DB= +b._emscripten_bind_btSoftBody_forceActivationState_1=b.asm.iz).apply(null,arguments)},EB=b._emscripten_bind_btSoftBody_activate_0=function(){return(EB=b._emscripten_bind_btSoftBody_activate_0=b.asm.jz).apply(null,arguments)},FB=b._emscripten_bind_btSoftBody_activate_1=function(){return(FB=b._emscripten_bind_btSoftBody_activate_1=b.asm.kz).apply(null,arguments)},GB=b._emscripten_bind_btSoftBody_isActive_0=function(){return(GB=b._emscripten_bind_btSoftBody_isActive_0=b.asm.lz).apply(null,arguments)}, +HB=b._emscripten_bind_btSoftBody_isKinematicObject_0=function(){return(HB=b._emscripten_bind_btSoftBody_isKinematicObject_0=b.asm.mz).apply(null,arguments)},IB=b._emscripten_bind_btSoftBody_isStaticObject_0=function(){return(IB=b._emscripten_bind_btSoftBody_isStaticObject_0=b.asm.nz).apply(null,arguments)},JB=b._emscripten_bind_btSoftBody_isStaticOrKinematicObject_0=function(){return(JB=b._emscripten_bind_btSoftBody_isStaticOrKinematicObject_0=b.asm.oz).apply(null,arguments)},KB=b._emscripten_bind_btSoftBody_getRestitution_0= +function(){return(KB=b._emscripten_bind_btSoftBody_getRestitution_0=b.asm.pz).apply(null,arguments)},LB=b._emscripten_bind_btSoftBody_getFriction_0=function(){return(LB=b._emscripten_bind_btSoftBody_getFriction_0=b.asm.qz).apply(null,arguments)},MB=b._emscripten_bind_btSoftBody_getRollingFriction_0=function(){return(MB=b._emscripten_bind_btSoftBody_getRollingFriction_0=b.asm.rz).apply(null,arguments)},NB=b._emscripten_bind_btSoftBody_setRestitution_1=function(){return(NB=b._emscripten_bind_btSoftBody_setRestitution_1= +b.asm.sz).apply(null,arguments)},OB=b._emscripten_bind_btSoftBody_setFriction_1=function(){return(OB=b._emscripten_bind_btSoftBody_setFriction_1=b.asm.tz).apply(null,arguments)},PB=b._emscripten_bind_btSoftBody_setRollingFriction_1=function(){return(PB=b._emscripten_bind_btSoftBody_setRollingFriction_1=b.asm.uz).apply(null,arguments)},QB=b._emscripten_bind_btSoftBody_getWorldTransform_0=function(){return(QB=b._emscripten_bind_btSoftBody_getWorldTransform_0=b.asm.vz).apply(null,arguments)},RB=b._emscripten_bind_btSoftBody_getCollisionFlags_0= +function(){return(RB=b._emscripten_bind_btSoftBody_getCollisionFlags_0=b.asm.wz).apply(null,arguments)},SB=b._emscripten_bind_btSoftBody_setCollisionFlags_1=function(){return(SB=b._emscripten_bind_btSoftBody_setCollisionFlags_1=b.asm.xz).apply(null,arguments)},TB=b._emscripten_bind_btSoftBody_setWorldTransform_1=function(){return(TB=b._emscripten_bind_btSoftBody_setWorldTransform_1=b.asm.yz).apply(null,arguments)},UB=b._emscripten_bind_btSoftBody_setCollisionShape_1=function(){return(UB=b._emscripten_bind_btSoftBody_setCollisionShape_1= +b.asm.zz).apply(null,arguments)},VB=b._emscripten_bind_btSoftBody_setCcdMotionThreshold_1=function(){return(VB=b._emscripten_bind_btSoftBody_setCcdMotionThreshold_1=b.asm.Az).apply(null,arguments)},WB=b._emscripten_bind_btSoftBody_setCcdSweptSphereRadius_1=function(){return(WB=b._emscripten_bind_btSoftBody_setCcdSweptSphereRadius_1=b.asm.Bz).apply(null,arguments)},XB=b._emscripten_bind_btSoftBody_getUserIndex_0=function(){return(XB=b._emscripten_bind_btSoftBody_getUserIndex_0=b.asm.Cz).apply(null, +arguments)},YB=b._emscripten_bind_btSoftBody_setUserIndex_1=function(){return(YB=b._emscripten_bind_btSoftBody_setUserIndex_1=b.asm.Dz).apply(null,arguments)},ZB=b._emscripten_bind_btSoftBody_getUserPointer_0=function(){return(ZB=b._emscripten_bind_btSoftBody_getUserPointer_0=b.asm.Ez).apply(null,arguments)},$B=b._emscripten_bind_btSoftBody_setUserPointer_1=function(){return($B=b._emscripten_bind_btSoftBody_setUserPointer_1=b.asm.Fz).apply(null,arguments)},aC=b._emscripten_bind_btSoftBody_getBroadphaseHandle_0= +function(){return(aC=b._emscripten_bind_btSoftBody_getBroadphaseHandle_0=b.asm.Gz).apply(null,arguments)},bC=b._emscripten_bind_btSoftBody_get_m_cfg_0=function(){return(bC=b._emscripten_bind_btSoftBody_get_m_cfg_0=b.asm.Hz).apply(null,arguments)},cC=b._emscripten_bind_btSoftBody_set_m_cfg_1=function(){return(cC=b._emscripten_bind_btSoftBody_set_m_cfg_1=b.asm.Iz).apply(null,arguments)},dC=b._emscripten_bind_btSoftBody_get_m_nodes_0=function(){return(dC=b._emscripten_bind_btSoftBody_get_m_nodes_0=b.asm.Jz).apply(null, +arguments)},eC=b._emscripten_bind_btSoftBody_set_m_nodes_1=function(){return(eC=b._emscripten_bind_btSoftBody_set_m_nodes_1=b.asm.Kz).apply(null,arguments)},fC=b._emscripten_bind_btSoftBody_get_m_faces_0=function(){return(fC=b._emscripten_bind_btSoftBody_get_m_faces_0=b.asm.Lz).apply(null,arguments)},gC=b._emscripten_bind_btSoftBody_set_m_faces_1=function(){return(gC=b._emscripten_bind_btSoftBody_set_m_faces_1=b.asm.Mz).apply(null,arguments)},hC=b._emscripten_bind_btSoftBody_get_m_materials_0=function(){return(hC= +b._emscripten_bind_btSoftBody_get_m_materials_0=b.asm.Nz).apply(null,arguments)},iC=b._emscripten_bind_btSoftBody_set_m_materials_1=function(){return(iC=b._emscripten_bind_btSoftBody_set_m_materials_1=b.asm.Oz).apply(null,arguments)},jC=b._emscripten_bind_btSoftBody_get_m_anchors_0=function(){return(jC=b._emscripten_bind_btSoftBody_get_m_anchors_0=b.asm.Pz).apply(null,arguments)},kC=b._emscripten_bind_btSoftBody_set_m_anchors_1=function(){return(kC=b._emscripten_bind_btSoftBody_set_m_anchors_1=b.asm.Qz).apply(null, +arguments)},lC=b._emscripten_bind_btSoftBody___destroy___0=function(){return(lC=b._emscripten_bind_btSoftBody___destroy___0=b.asm.Rz).apply(null,arguments)},mC=b._emscripten_bind_btSoftBodyRigidBodyCollisionConfiguration_btSoftBodyRigidBodyCollisionConfiguration_0=function(){return(mC=b._emscripten_bind_btSoftBodyRigidBodyCollisionConfiguration_btSoftBodyRigidBodyCollisionConfiguration_0=b.asm.Sz).apply(null,arguments)},nC=b._emscripten_bind_btSoftBodyRigidBodyCollisionConfiguration_btSoftBodyRigidBodyCollisionConfiguration_1= +function(){return(nC=b._emscripten_bind_btSoftBodyRigidBodyCollisionConfiguration_btSoftBodyRigidBodyCollisionConfiguration_1=b.asm.Tz).apply(null,arguments)},oC=b._emscripten_bind_btSoftBodyRigidBodyCollisionConfiguration___destroy___0=function(){return(oC=b._emscripten_bind_btSoftBodyRigidBodyCollisionConfiguration___destroy___0=b.asm.Uz).apply(null,arguments)},pC=b._emscripten_bind_btDefaultSoftBodySolver_btDefaultSoftBodySolver_0=function(){return(pC=b._emscripten_bind_btDefaultSoftBodySolver_btDefaultSoftBodySolver_0= +b.asm.Vz).apply(null,arguments)},qC=b._emscripten_bind_btDefaultSoftBodySolver___destroy___0=function(){return(qC=b._emscripten_bind_btDefaultSoftBodySolver___destroy___0=b.asm.Wz).apply(null,arguments)},rC=b._emscripten_bind_btSoftBodyArray_size_0=function(){return(rC=b._emscripten_bind_btSoftBodyArray_size_0=b.asm.Xz).apply(null,arguments)},sC=b._emscripten_bind_btSoftBodyArray_at_1=function(){return(sC=b._emscripten_bind_btSoftBodyArray_at_1=b.asm.Yz).apply(null,arguments)},tC=b._emscripten_bind_btSoftBodyArray___destroy___0= +function(){return(tC=b._emscripten_bind_btSoftBodyArray___destroy___0=b.asm.Zz).apply(null,arguments)},uC=b._emscripten_bind_btSoftRigidDynamicsWorld_btSoftRigidDynamicsWorld_5=function(){return(uC=b._emscripten_bind_btSoftRigidDynamicsWorld_btSoftRigidDynamicsWorld_5=b.asm._z).apply(null,arguments)},vC=b._emscripten_bind_btSoftRigidDynamicsWorld_addSoftBody_3=function(){return(vC=b._emscripten_bind_btSoftRigidDynamicsWorld_addSoftBody_3=b.asm.$z).apply(null,arguments)},wC=b._emscripten_bind_btSoftRigidDynamicsWorld_removeSoftBody_1= +function(){return(wC=b._emscripten_bind_btSoftRigidDynamicsWorld_removeSoftBody_1=b.asm.aA).apply(null,arguments)},xC=b._emscripten_bind_btSoftRigidDynamicsWorld_removeCollisionObject_1=function(){return(xC=b._emscripten_bind_btSoftRigidDynamicsWorld_removeCollisionObject_1=b.asm.bA).apply(null,arguments)},yC=b._emscripten_bind_btSoftRigidDynamicsWorld_getWorldInfo_0=function(){return(yC=b._emscripten_bind_btSoftRigidDynamicsWorld_getWorldInfo_0=b.asm.cA).apply(null,arguments)},zC=b._emscripten_bind_btSoftRigidDynamicsWorld_getSoftBodyArray_0= +function(){return(zC=b._emscripten_bind_btSoftRigidDynamicsWorld_getSoftBodyArray_0=b.asm.dA).apply(null,arguments)},AC=b._emscripten_bind_btSoftRigidDynamicsWorld_getDispatcher_0=function(){return(AC=b._emscripten_bind_btSoftRigidDynamicsWorld_getDispatcher_0=b.asm.eA).apply(null,arguments)},BC=b._emscripten_bind_btSoftRigidDynamicsWorld_rayTest_3=function(){return(BC=b._emscripten_bind_btSoftRigidDynamicsWorld_rayTest_3=b.asm.fA).apply(null,arguments)},CC=b._emscripten_bind_btSoftRigidDynamicsWorld_getPairCache_0= +function(){return(CC=b._emscripten_bind_btSoftRigidDynamicsWorld_getPairCache_0=b.asm.gA).apply(null,arguments)},DC=b._emscripten_bind_btSoftRigidDynamicsWorld_getDispatchInfo_0=function(){return(DC=b._emscripten_bind_btSoftRigidDynamicsWorld_getDispatchInfo_0=b.asm.hA).apply(null,arguments)},EC=b._emscripten_bind_btSoftRigidDynamicsWorld_addCollisionObject_1=function(){return(EC=b._emscripten_bind_btSoftRigidDynamicsWorld_addCollisionObject_1=b.asm.iA).apply(null,arguments)},FC=b._emscripten_bind_btSoftRigidDynamicsWorld_addCollisionObject_2= +function(){return(FC=b._emscripten_bind_btSoftRigidDynamicsWorld_addCollisionObject_2=b.asm.jA).apply(null,arguments)},GC=b._emscripten_bind_btSoftRigidDynamicsWorld_addCollisionObject_3=function(){return(GC=b._emscripten_bind_btSoftRigidDynamicsWorld_addCollisionObject_3=b.asm.kA).apply(null,arguments)},HC=b._emscripten_bind_btSoftRigidDynamicsWorld_getBroadphase_0=function(){return(HC=b._emscripten_bind_btSoftRigidDynamicsWorld_getBroadphase_0=b.asm.lA).apply(null,arguments)},IC=b._emscripten_bind_btSoftRigidDynamicsWorld_convexSweepTest_5= +function(){return(IC=b._emscripten_bind_btSoftRigidDynamicsWorld_convexSweepTest_5=b.asm.mA).apply(null,arguments)},JC=b._emscripten_bind_btSoftRigidDynamicsWorld_contactPairTest_3=function(){return(JC=b._emscripten_bind_btSoftRigidDynamicsWorld_contactPairTest_3=b.asm.nA).apply(null,arguments)},KC=b._emscripten_bind_btSoftRigidDynamicsWorld_contactTest_2=function(){return(KC=b._emscripten_bind_btSoftRigidDynamicsWorld_contactTest_2=b.asm.oA).apply(null,arguments)},LC=b._emscripten_bind_btSoftRigidDynamicsWorld_updateSingleAabb_1= +function(){return(LC=b._emscripten_bind_btSoftRigidDynamicsWorld_updateSingleAabb_1=b.asm.pA).apply(null,arguments)},MC=b._emscripten_bind_btSoftRigidDynamicsWorld_setDebugDrawer_1=function(){return(MC=b._emscripten_bind_btSoftRigidDynamicsWorld_setDebugDrawer_1=b.asm.qA).apply(null,arguments)},NC=b._emscripten_bind_btSoftRigidDynamicsWorld_getDebugDrawer_0=function(){return(NC=b._emscripten_bind_btSoftRigidDynamicsWorld_getDebugDrawer_0=b.asm.rA).apply(null,arguments)},OC=b._emscripten_bind_btSoftRigidDynamicsWorld_debugDrawWorld_0= +function(){return(OC=b._emscripten_bind_btSoftRigidDynamicsWorld_debugDrawWorld_0=b.asm.sA).apply(null,arguments)},PC=b._emscripten_bind_btSoftRigidDynamicsWorld_debugDrawObject_3=function(){return(PC=b._emscripten_bind_btSoftRigidDynamicsWorld_debugDrawObject_3=b.asm.tA).apply(null,arguments)},QC=b._emscripten_bind_btSoftRigidDynamicsWorld_setGravity_1=function(){return(QC=b._emscripten_bind_btSoftRigidDynamicsWorld_setGravity_1=b.asm.uA).apply(null,arguments)},RC=b._emscripten_bind_btSoftRigidDynamicsWorld_getGravity_0= +function(){return(RC=b._emscripten_bind_btSoftRigidDynamicsWorld_getGravity_0=b.asm.vA).apply(null,arguments)},SC=b._emscripten_bind_btSoftRigidDynamicsWorld_addRigidBody_1=function(){return(SC=b._emscripten_bind_btSoftRigidDynamicsWorld_addRigidBody_1=b.asm.wA).apply(null,arguments)},TC=b._emscripten_bind_btSoftRigidDynamicsWorld_addRigidBody_3=function(){return(TC=b._emscripten_bind_btSoftRigidDynamicsWorld_addRigidBody_3=b.asm.xA).apply(null,arguments)},UC=b._emscripten_bind_btSoftRigidDynamicsWorld_removeRigidBody_1= +function(){return(UC=b._emscripten_bind_btSoftRigidDynamicsWorld_removeRigidBody_1=b.asm.yA).apply(null,arguments)},VC=b._emscripten_bind_btSoftRigidDynamicsWorld_addConstraint_1=function(){return(VC=b._emscripten_bind_btSoftRigidDynamicsWorld_addConstraint_1=b.asm.zA).apply(null,arguments)},WC=b._emscripten_bind_btSoftRigidDynamicsWorld_addConstraint_2=function(){return(WC=b._emscripten_bind_btSoftRigidDynamicsWorld_addConstraint_2=b.asm.AA).apply(null,arguments)},XC=b._emscripten_bind_btSoftRigidDynamicsWorld_removeConstraint_1= +function(){return(XC=b._emscripten_bind_btSoftRigidDynamicsWorld_removeConstraint_1=b.asm.BA).apply(null,arguments)},YC=b._emscripten_bind_btSoftRigidDynamicsWorld_stepSimulation_1=function(){return(YC=b._emscripten_bind_btSoftRigidDynamicsWorld_stepSimulation_1=b.asm.CA).apply(null,arguments)},ZC=b._emscripten_bind_btSoftRigidDynamicsWorld_stepSimulation_2=function(){return(ZC=b._emscripten_bind_btSoftRigidDynamicsWorld_stepSimulation_2=b.asm.DA).apply(null,arguments)},$C=b._emscripten_bind_btSoftRigidDynamicsWorld_stepSimulation_3= +function(){return($C=b._emscripten_bind_btSoftRigidDynamicsWorld_stepSimulation_3=b.asm.EA).apply(null,arguments)},aD=b._emscripten_bind_btSoftRigidDynamicsWorld_setContactAddedCallback_1=function(){return(aD=b._emscripten_bind_btSoftRigidDynamicsWorld_setContactAddedCallback_1=b.asm.FA).apply(null,arguments)},bD=b._emscripten_bind_btSoftRigidDynamicsWorld_setContactProcessedCallback_1=function(){return(bD=b._emscripten_bind_btSoftRigidDynamicsWorld_setContactProcessedCallback_1=b.asm.GA).apply(null, +arguments)},cD=b._emscripten_bind_btSoftRigidDynamicsWorld_setContactDestroyedCallback_1=function(){return(cD=b._emscripten_bind_btSoftRigidDynamicsWorld_setContactDestroyedCallback_1=b.asm.HA).apply(null,arguments)},dD=b._emscripten_bind_btSoftRigidDynamicsWorld_addAction_1=function(){return(dD=b._emscripten_bind_btSoftRigidDynamicsWorld_addAction_1=b.asm.IA).apply(null,arguments)},eD=b._emscripten_bind_btSoftRigidDynamicsWorld_removeAction_1=function(){return(eD=b._emscripten_bind_btSoftRigidDynamicsWorld_removeAction_1= +b.asm.JA).apply(null,arguments)},fD=b._emscripten_bind_btSoftRigidDynamicsWorld_getSolverInfo_0=function(){return(fD=b._emscripten_bind_btSoftRigidDynamicsWorld_getSolverInfo_0=b.asm.KA).apply(null,arguments)},gD=b._emscripten_bind_btSoftRigidDynamicsWorld_setInternalTickCallback_1=function(){return(gD=b._emscripten_bind_btSoftRigidDynamicsWorld_setInternalTickCallback_1=b.asm.LA).apply(null,arguments)},hD=b._emscripten_bind_btSoftRigidDynamicsWorld_setInternalTickCallback_2=function(){return(hD= +b._emscripten_bind_btSoftRigidDynamicsWorld_setInternalTickCallback_2=b.asm.MA).apply(null,arguments)},iD=b._emscripten_bind_btSoftRigidDynamicsWorld_setInternalTickCallback_3=function(){return(iD=b._emscripten_bind_btSoftRigidDynamicsWorld_setInternalTickCallback_3=b.asm.NA).apply(null,arguments)},jD=b._emscripten_bind_btSoftRigidDynamicsWorld___destroy___0=function(){return(jD=b._emscripten_bind_btSoftRigidDynamicsWorld___destroy___0=b.asm.OA).apply(null,arguments)},kD=b._emscripten_bind_btSoftBodyHelpers_btSoftBodyHelpers_0= +function(){return(kD=b._emscripten_bind_btSoftBodyHelpers_btSoftBodyHelpers_0=b.asm.PA).apply(null,arguments)},lD=b._emscripten_bind_btSoftBodyHelpers_CreateRope_5=function(){return(lD=b._emscripten_bind_btSoftBodyHelpers_CreateRope_5=b.asm.QA).apply(null,arguments)},mD=b._emscripten_bind_btSoftBodyHelpers_CreatePatch_9=function(){return(mD=b._emscripten_bind_btSoftBodyHelpers_CreatePatch_9=b.asm.RA).apply(null,arguments)},nD=b._emscripten_bind_btSoftBodyHelpers_CreatePatchUV_10=function(){return(nD= +b._emscripten_bind_btSoftBodyHelpers_CreatePatchUV_10=b.asm.SA).apply(null,arguments)},oD=b._emscripten_bind_btSoftBodyHelpers_CreateEllipsoid_4=function(){return(oD=b._emscripten_bind_btSoftBodyHelpers_CreateEllipsoid_4=b.asm.TA).apply(null,arguments)},pD=b._emscripten_bind_btSoftBodyHelpers_CreateFromTriMesh_5=function(){return(pD=b._emscripten_bind_btSoftBodyHelpers_CreateFromTriMesh_5=b.asm.UA).apply(null,arguments)},qD=b._emscripten_bind_btSoftBodyHelpers_CreateFromConvexHull_4=function(){return(qD= +b._emscripten_bind_btSoftBodyHelpers_CreateFromConvexHull_4=b.asm.VA).apply(null,arguments)},rD=b._emscripten_bind_btSoftBodyHelpers___destroy___0=function(){return(rD=b._emscripten_bind_btSoftBodyHelpers___destroy___0=b.asm.WA).apply(null,arguments)},sD=b._emscripten_enum_PHY_ScalarType_PHY_FLOAT=function(){return(sD=b._emscripten_enum_PHY_ScalarType_PHY_FLOAT=b.asm.XA).apply(null,arguments)},tD=b._emscripten_enum_PHY_ScalarType_PHY_DOUBLE=function(){return(tD=b._emscripten_enum_PHY_ScalarType_PHY_DOUBLE= +b.asm.YA).apply(null,arguments)},uD=b._emscripten_enum_PHY_ScalarType_PHY_INTEGER=function(){return(uD=b._emscripten_enum_PHY_ScalarType_PHY_INTEGER=b.asm.ZA).apply(null,arguments)},vD=b._emscripten_enum_PHY_ScalarType_PHY_SHORT=function(){return(vD=b._emscripten_enum_PHY_ScalarType_PHY_SHORT=b.asm._A).apply(null,arguments)},wD=b._emscripten_enum_PHY_ScalarType_PHY_FIXEDPOINT88=function(){return(wD=b._emscripten_enum_PHY_ScalarType_PHY_FIXEDPOINT88=b.asm.$A).apply(null,arguments)},xD=b._emscripten_enum_PHY_ScalarType_PHY_UCHAR= +function(){return(xD=b._emscripten_enum_PHY_ScalarType_PHY_UCHAR=b.asm.aB).apply(null,arguments)},yD=b._emscripten_enum_eGIMPACT_SHAPE_TYPE_CONST_GIMPACT_COMPOUND_SHAPE=function(){return(yD=b._emscripten_enum_eGIMPACT_SHAPE_TYPE_CONST_GIMPACT_COMPOUND_SHAPE=b.asm.bB).apply(null,arguments)},zD=b._emscripten_enum_eGIMPACT_SHAPE_TYPE_CONST_GIMPACT_TRIMESH_SHAPE_PART=function(){return(zD=b._emscripten_enum_eGIMPACT_SHAPE_TYPE_CONST_GIMPACT_TRIMESH_SHAPE_PART=b.asm.cB).apply(null,arguments)},AD=b._emscripten_enum_eGIMPACT_SHAPE_TYPE_CONST_GIMPACT_TRIMESH_SHAPE= +function(){return(AD=b._emscripten_enum_eGIMPACT_SHAPE_TYPE_CONST_GIMPACT_TRIMESH_SHAPE=b.asm.dB).apply(null,arguments)},BD=b._emscripten_enum_btConstraintParams_BT_CONSTRAINT_ERP=function(){return(BD=b._emscripten_enum_btConstraintParams_BT_CONSTRAINT_ERP=b.asm.eB).apply(null,arguments)},CD=b._emscripten_enum_btConstraintParams_BT_CONSTRAINT_STOP_ERP=function(){return(CD=b._emscripten_enum_btConstraintParams_BT_CONSTRAINT_STOP_ERP=b.asm.fB).apply(null,arguments)},DD=b._emscripten_enum_btConstraintParams_BT_CONSTRAINT_CFM= +function(){return(DD=b._emscripten_enum_btConstraintParams_BT_CONSTRAINT_CFM=b.asm.gB).apply(null,arguments)},ED=b._emscripten_enum_btConstraintParams_BT_CONSTRAINT_STOP_CFM=function(){return(ED=b._emscripten_enum_btConstraintParams_BT_CONSTRAINT_STOP_CFM=b.asm.hB).apply(null,arguments)};b._malloc=function(){return(b._malloc=b.asm.jB).apply(null,arguments)};b.___start_em_js=27240;b.___stop_em_js=27338;b.UTF8ToString=sa; +b.addFunction=function(a,c){if(!Ua){Ua=new WeakMap;var d=ya.length;if(Ua)for(var e=0;e<0+d;e++){var g=e;var n=Ta[g];n||(g>=Ta.length&&(Ta.length=g+1),Ta[g]=n=ya.get(g));(g=n)&&Ua.set(g,e)}}if(d=Ua.get(a)||0)return d;if(Va.length)d=Va.pop();else{try{ya.grow(1)}catch(T){if(!(T instanceof RangeError))throw T;throw"Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.";}d=ya.length-1}try{e=d,ya.set(e,a),Ta[e]=ya.get(e)}catch(T){if(!(T instanceof TypeError))throw T;if("function"==typeof WebAssembly.Function){e= +WebAssembly.Function;g={i:"i32",j:"i32",f:"f32",d:"f64",p:"i32"};n={parameters:[],results:"v"==c[0]?[]:[g[c[0]]]};for(var z=1;zz?e.push(z):e.push(z%128|128,z>>7);for(z=0;zg?c.push(g):c.push(g%128|128,g>>7);c.push.apply(c, +e);c.push(2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0);c=new WebAssembly.Module(new Uint8Array(c));c=(new WebAssembly.Instance(c,{e:{f:a}})).exports.f}e=d;ya.set(e,c);Ta[e]=ya.get(e)}Ua.set(a,d);return d};var FD;Ha=function GD(){FD||HD();FD||(Ha=GD)}; +function HD(){function a(){if(!FD&&(FD=!0,b.calledRun=!0,!qa)){Ca=!0;Qa(Aa);aa(b);if(b.onRuntimeInitialized)b.onRuntimeInitialized();if(b.postRun)for("function"==typeof b.postRun&&(b.postRun=[b.postRun]);b.postRun.length;){var c=b.postRun.shift();Ba.unshift(c)}Qa(Ba)}}if(!(0=JD?(0>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var e=0;e=e?c++:2047>=e?c+=2:55296<=e&&57343>=e?(c+=4,++d):c+=3}c=Array(c+1);e=c.length;d=0;if(0=n){var z=a.charCodeAt(++g);n=65536+((n&1023)<<10)|z&1023}if(127>=n){if(d>=e)break;c[d++]=n}else{if(2047>=n){if(d+1>=e)break;c[d++]=192|n>>6}else{if(65535>=n){if(d+2>=e)break;c[d++]=224|n>>12}else{if(d+3>=e)break;c[d++]=240|n>>18;c[d++]= +128|n>>12&63}c[d++]=128|n>>6&63}c[d++]=128|n&63}}c[d]=0}a=OD(c,ua);PD(c,ua,a);return a}return a}function RD(a){if("object"===typeof a){var c=OD(a,wa);PD(a,wa,c);return c}return a}function l(){throw"cannot construct a btCollisionShape, no constructor in IDL";}l.prototype=Object.create(f.prototype);l.prototype.constructor=l;l.prototype.lB=l;l.mB={};b.btCollisionShape=l;l.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Xa(c,a)}; +l.prototype.getLocalScaling=function(){return k(Ya(this.kB),m)};l.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Za(d,a,c)};l.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$a(c,a)};l.prototype.getMargin=function(){return ab(this.kB)};l.prototype.__destroy__=function(){bb(this.kB)};function SD(){throw"cannot construct a btCollisionWorld, no constructor in IDL";}SD.prototype=Object.create(f.prototype); +SD.prototype.constructor=SD;SD.prototype.lB=SD;SD.mB={};b.btCollisionWorld=SD;SD.prototype.getDispatcher=function(){return k(cb(this.kB),TD)};SD.prototype.rayTest=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);db(e,a,c,d)};SD.prototype.getPairCache=function(){return k(eb(this.kB),UD)};SD.prototype.getDispatchInfo=function(){return k(fb(this.kB),p)}; +SD.prototype.addCollisionObject=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?gb(e,a):void 0===d?hb(e,a,c):ib(e,a,c,d)};SD.prototype.removeCollisionObject=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jb(c,a)};SD.prototype.getBroadphase=function(){return k(kb(this.kB),VD)}; +SD.prototype.convexSweepTest=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);lb(n,a,c,d,e,g)};SD.prototype.contactPairTest=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);mb(e,a,c,d)}; +SD.prototype.contactTest=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);nb(d,a,c)};SD.prototype.updateSingleAabb=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ob(c,a)};SD.prototype.setDebugDrawer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pb(c,a)};SD.prototype.getDebugDrawer=function(){return k(qb(this.kB),WD)};SD.prototype.debugDrawWorld=function(){rb(this.kB)}; +SD.prototype.debugDrawObject=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);sb(e,a,c,d)};SD.prototype.__destroy__=function(){tb(this.kB)};function q(){throw"cannot construct a btCollisionObject, no constructor in IDL";}q.prototype=Object.create(f.prototype);q.prototype.constructor=q;q.prototype.lB=q;q.mB={};b.btCollisionObject=q; +q.prototype.setAnisotropicFriction=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);ub(d,a,c)};q.prototype.getCollisionShape=function(){return k(vb(this.kB),l)};q.prototype.setContactProcessingThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);wb(c,a)};q.prototype.setActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xb(c,a)}; +q.prototype.forceActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yb(c,a)};q.prototype.activate=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);void 0===a?zb(c):Ab(c,a)};q.prototype.isActive=function(){return!!Bb(this.kB)};q.prototype.isKinematicObject=function(){return!!Cb(this.kB)};q.prototype.isStaticObject=function(){return!!Db(this.kB)};q.prototype.isStaticOrKinematicObject=function(){return!!Eb(this.kB)};q.prototype.getRestitution=function(){return Fb(this.kB)}; +q.prototype.getFriction=function(){return Gb(this.kB)};q.prototype.getRollingFriction=function(){return Hb(this.kB)};q.prototype.setRestitution=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ib(c,a)};q.prototype.setFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jb(c,a)};q.prototype.setRollingFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Kb(c,a)};q.prototype.getWorldTransform=function(){return k(Lb(this.kB),r)};q.prototype.getCollisionFlags=function(){return Mb(this.kB)}; +q.prototype.setCollisionFlags=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Nb(c,a)};q.prototype.setWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ob(c,a)};q.prototype.setCollisionShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Pb(c,a)};q.prototype.setCcdMotionThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Qb(c,a)};q.prototype.setCcdSweptSphereRadius=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Rb(c,a)}; +q.prototype.getUserIndex=function(){return Sb(this.kB)};q.prototype.setUserIndex=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Tb(c,a)};q.prototype.getUserPointer=function(){return k(Ub(this.kB),XD)};q.prototype.setUserPointer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vb(c,a)};q.prototype.getBroadphaseHandle=function(){return k(Wb(this.kB),YD)};q.prototype.__destroy__=function(){Xb(this.kB)}; +function ZD(){throw"cannot construct a btConcaveShape, no constructor in IDL";}ZD.prototype=Object.create(l.prototype);ZD.prototype.constructor=ZD;ZD.prototype.lB=ZD;ZD.mB={};b.btConcaveShape=ZD;ZD.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Yb(c,a)};ZD.prototype.getLocalScaling=function(){return k(Zb(this.kB),m)};ZD.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);$b(d,a,c)}; +ZD.prototype.__destroy__=function(){ac(this.kB)};function $D(){throw"cannot construct a btCollisionAlgorithm, no constructor in IDL";}$D.prototype=Object.create(f.prototype);$D.prototype.constructor=$D;$D.prototype.lB=$D;$D.mB={};b.btCollisionAlgorithm=$D;$D.prototype.__destroy__=function(){bc(this.kB)};function aE(){throw"cannot construct a btTypedConstraint, no constructor in IDL";}aE.prototype=Object.create(f.prototype);aE.prototype.constructor=aE;aE.prototype.lB=aE;aE.mB={}; +b.btTypedConstraint=aE;aE.prototype.enableFeedback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);cc(c,a)};aE.prototype.getBreakingImpulseThreshold=function(){return ec(this.kB)};aE.prototype.setBreakingImpulseThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fc(c,a)};aE.prototype.getParam=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return gc(d,a,c)}; +aE.prototype.setParam=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);hc(e,a,c,d)};aE.prototype.__destroy__=function(){ic(this.kB)};function bE(){throw"cannot construct a btDynamicsWorld, no constructor in IDL";}bE.prototype=Object.create(SD.prototype);bE.prototype.constructor=bE;bE.prototype.lB=bE;bE.mB={};b.btDynamicsWorld=bE;bE.prototype.addAction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jc(c,a)}; +bE.prototype.removeAction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);kc(c,a)};bE.prototype.getSolverInfo=function(){return k(lc(this.kB),t)};bE.prototype.setInternalTickCallback=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?mc(e,a):void 0===d?nc(e,a,c):oc(e,a,c,d)};bE.prototype.getDispatcher=function(){return k(pc(this.kB),TD)}; +bE.prototype.rayTest=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);qc(e,a,c,d)};bE.prototype.getPairCache=function(){return k(rc(this.kB),UD)};bE.prototype.getDispatchInfo=function(){return k(sc(this.kB),p)}; +bE.prototype.addCollisionObject=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?tc(e,a):void 0===d?uc(e,a,c):vc(e,a,c,d)};bE.prototype.removeCollisionObject=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);wc(c,a)};bE.prototype.getBroadphase=function(){return k(xc(this.kB),VD)}; +bE.prototype.convexSweepTest=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);yc(n,a,c,d,e,g)};bE.prototype.contactPairTest=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);zc(e,a,c,d)}; +bE.prototype.contactTest=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Ac(d,a,c)};bE.prototype.updateSingleAabb=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Bc(c,a)};bE.prototype.setDebugDrawer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Cc(c,a)};bE.prototype.getDebugDrawer=function(){return k(Dc(this.kB),WD)};bE.prototype.debugDrawWorld=function(){Ec(this.kB)}; +bE.prototype.debugDrawObject=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Fc(e,a,c,d)};bE.prototype.__destroy__=function(){Gc(this.kB)};function WD(){throw"cannot construct a btIDebugDraw, no constructor in IDL";}WD.prototype=Object.create(f.prototype);WD.prototype.constructor=WD;WD.prototype.lB=WD;WD.mB={};b.btIDebugDraw=WD; +WD.prototype.drawLine=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Hc(e,a,c,d)};WD.prototype.drawContactPoint=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);Ic(n,a,c,d,e,g)}; +WD.prototype.reportErrorWarning=function(a){var c=this.kB;ND();a=a&&"object"===typeof a?a.kB:QD(a);Jc(c,a)};WD.prototype.draw3dText=function(a,c){var d=this.kB;ND();a&&"object"===typeof a&&(a=a.kB);c=c&&"object"===typeof c?c.kB:QD(c);Kc(d,a,c)};WD.prototype.setDebugMode=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Lc(c,a)};WD.prototype.getDebugMode=function(){return Mc(this.kB)};WD.prototype.__destroy__=function(){Nc(this.kB)}; +function m(a,c,d){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);this.kB=void 0===a?Oc():void 0===c?_emscripten_bind_btVector3_btVector3_1(a):void 0===d?_emscripten_bind_btVector3_btVector3_2(a,c):Pc(a,c,d);h(m)[this.kB]=this}m.prototype=Object.create(f.prototype);m.prototype.constructor=m;m.prototype.lB=m;m.mB={};b.btVector3=m;m.prototype.length=m.prototype.length=function(){return Qc(this.kB)};m.prototype.x=m.prototype.x=function(){return Rc(this.kB)}; +m.prototype.y=m.prototype.y=function(){return Sc(this.kB)};m.prototype.z=m.prototype.z=function(){return Tc(this.kB)};m.prototype.setX=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Uc(c,a)};m.prototype.setY=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vc(c,a)};m.prototype.setZ=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Wc(c,a)}; +m.prototype.setValue=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Xc(e,a,c,d)};m.prototype.normalize=m.prototype.normalize=function(){Yc(this.kB)};m.prototype.rotate=m.prototype.rotate=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return k(Zc(d,a,c),m)};m.prototype.dot=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return $c(c,a)}; +m.prototype.op_mul=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(ad(c,a),m)};m.prototype.op_add=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(bd(c,a),m)};m.prototype.op_sub=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(cd(c,a),m)};m.prototype.__destroy__=function(){dd(this.kB)};function cE(){throw"cannot construct a btQuadWord, no constructor in IDL";}cE.prototype=Object.create(f.prototype);cE.prototype.constructor=cE; +cE.prototype.lB=cE;cE.mB={};b.btQuadWord=cE;cE.prototype.x=cE.prototype.x=function(){return ed(this.kB)};cE.prototype.y=cE.prototype.y=function(){return fd(this.kB)};cE.prototype.z=cE.prototype.z=function(){return gd(this.kB)};cE.prototype.w=cE.prototype.w=function(){return hd(this.kB)};cE.prototype.setX=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jd(c,a)};cE.prototype.setY=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);kd(c,a)}; +cE.prototype.setZ=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ld(c,a)};cE.prototype.setW=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);md(c,a)};cE.prototype.__destroy__=function(){nd(this.kB)};function dE(){throw"cannot construct a btMotionState, no constructor in IDL";}dE.prototype=Object.create(f.prototype);dE.prototype.constructor=dE;dE.prototype.lB=dE;dE.mB={};b.btMotionState=dE; +dE.prototype.getWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);od(c,a)};dE.prototype.setWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pd(c,a)};dE.prototype.__destroy__=function(){qd(this.kB)};function u(){throw"cannot construct a RayResultCallback, no constructor in IDL";}u.prototype=Object.create(f.prototype);u.prototype.constructor=u;u.prototype.lB=u;u.mB={};b.RayResultCallback=u;u.prototype.hasHit=function(){return!!rd(this.kB)}; +u.prototype.get_m_collisionFilterGroup=u.prototype.nB=function(){return sd(this.kB)};u.prototype.set_m_collisionFilterGroup=u.prototype.pB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);td(c,a)};Object.defineProperty(u.prototype,"m_collisionFilterGroup",{get:u.prototype.nB,set:u.prototype.pB});u.prototype.get_m_collisionFilterMask=u.prototype.oB=function(){return ud(this.kB)}; +u.prototype.set_m_collisionFilterMask=u.prototype.qB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vd(c,a)};Object.defineProperty(u.prototype,"m_collisionFilterMask",{get:u.prototype.oB,set:u.prototype.qB});u.prototype.get_m_closestHitFraction=u.prototype.rB=function(){return wd(this.kB)};u.prototype.set_m_closestHitFraction=u.prototype.sB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xd(c,a)};Object.defineProperty(u.prototype,"m_closestHitFraction",{get:u.prototype.rB,set:u.prototype.sB}); +u.prototype.get_m_collisionObject=u.prototype.vB=function(){return k(yd(this.kB),q)};u.prototype.set_m_collisionObject=u.prototype.CB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zd(c,a)};Object.defineProperty(u.prototype,"m_collisionObject",{get:u.prototype.vB,set:u.prototype.CB});u.prototype.get_m_flags=u.prototype.tB=function(){return Ad(this.kB)};u.prototype.set_m_flags=u.prototype.uB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Bd(c,a)}; +Object.defineProperty(u.prototype,"m_flags",{get:u.prototype.tB,set:u.prototype.uB});u.prototype.__destroy__=function(){Cd(this.kB)};function eE(){throw"cannot construct a ContactResultCallback, no constructor in IDL";}eE.prototype=Object.create(f.prototype);eE.prototype.constructor=eE;eE.prototype.lB=eE;eE.mB={};b.ContactResultCallback=eE; +eE.prototype.addSingleResult=function(a,c,d,e,g,n,z){var T=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);n&&"object"===typeof n&&(n=n.kB);z&&"object"===typeof z&&(z=z.kB);return Dd(T,a,c,d,e,g,n,z)};eE.prototype.__destroy__=function(){Ed(this.kB)};function v(){throw"cannot construct a ConvexResultCallback, no constructor in IDL";}v.prototype=Object.create(f.prototype); +v.prototype.constructor=v;v.prototype.lB=v;v.mB={};b.ConvexResultCallback=v;v.prototype.hasHit=function(){return!!Fd(this.kB)};v.prototype.get_m_collisionFilterGroup=v.prototype.nB=function(){return Gd(this.kB)};v.prototype.set_m_collisionFilterGroup=v.prototype.pB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Hd(c,a)};Object.defineProperty(v.prototype,"m_collisionFilterGroup",{get:v.prototype.nB,set:v.prototype.pB});v.prototype.get_m_collisionFilterMask=v.prototype.oB=function(){return Id(this.kB)}; +v.prototype.set_m_collisionFilterMask=v.prototype.qB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jd(c,a)};Object.defineProperty(v.prototype,"m_collisionFilterMask",{get:v.prototype.oB,set:v.prototype.qB});v.prototype.get_m_closestHitFraction=v.prototype.rB=function(){return Kd(this.kB)};v.prototype.set_m_closestHitFraction=v.prototype.sB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ld(c,a)};Object.defineProperty(v.prototype,"m_closestHitFraction",{get:v.prototype.rB,set:v.prototype.sB}); +v.prototype.__destroy__=function(){Md(this.kB)};function fE(){throw"cannot construct a btConvexShape, no constructor in IDL";}fE.prototype=Object.create(l.prototype);fE.prototype.constructor=fE;fE.prototype.lB=fE;fE.mB={};b.btConvexShape=fE;fE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Nd(c,a)};fE.prototype.getLocalScaling=function(){return k(Od(this.kB),m)}; +fE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Pd(d,a,c)};fE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Qd(c,a)};fE.prototype.getMargin=function(){return Rd(this.kB)};fE.prototype.__destroy__=function(){Sd(this.kB)};function gE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=Td(a,c);h(gE)[this.kB]=this}gE.prototype=Object.create(l.prototype); +gE.prototype.constructor=gE;gE.prototype.lB=gE;gE.mB={};b.btCapsuleShape=gE;gE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ud(c,a)};gE.prototype.getMargin=function(){return Vd(this.kB)};gE.prototype.getUpAxis=function(){return Wd(this.kB)};gE.prototype.getRadius=function(){return Xd(this.kB)};gE.prototype.getHalfHeight=function(){return Yd(this.kB)};gE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Zd(c,a)}; +gE.prototype.getLocalScaling=function(){return k($d(this.kB),m)};gE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);ae(d,a,c)};gE.prototype.__destroy__=function(){be(this.kB)};function hE(a){a&&"object"===typeof a&&(a=a.kB);this.kB=ce(a);h(hE)[this.kB]=this}hE.prototype=Object.create(l.prototype);hE.prototype.constructor=hE;hE.prototype.lB=hE;hE.mB={};b.btCylinderShape=hE; +hE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);de(c,a)};hE.prototype.getMargin=function(){return ee(this.kB)};hE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fe(c,a)};hE.prototype.getLocalScaling=function(){return k(ge(this.kB),m)};hE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);he(d,a,c)};hE.prototype.__destroy__=function(){ie(this.kB)}; +function iE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=je(a,c);h(iE)[this.kB]=this}iE.prototype=Object.create(l.prototype);iE.prototype.constructor=iE;iE.prototype.lB=iE;iE.mB={};b.btConeShape=iE;iE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ke(c,a)};iE.prototype.getLocalScaling=function(){return k(le(this.kB),m)}; +iE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);me(d,a,c)};iE.prototype.__destroy__=function(){ne(this.kB)};function jE(){throw"cannot construct a btStridingMeshInterface, no constructor in IDL";}jE.prototype=Object.create(f.prototype);jE.prototype.constructor=jE;jE.prototype.lB=jE;jE.mB={};b.btStridingMeshInterface=jE;jE.prototype.setScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);oe(c,a)}; +jE.prototype.__destroy__=function(){pe(this.kB)};function kE(){throw"cannot construct a btTriangleMeshShape, no constructor in IDL";}kE.prototype=Object.create(ZD.prototype);kE.prototype.constructor=kE;kE.prototype.lB=kE;kE.mB={};b.btTriangleMeshShape=kE;kE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);qe(c,a)};kE.prototype.getLocalScaling=function(){return k(re(this.kB),m)}; +kE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);se(d,a,c)};kE.prototype.__destroy__=function(){te(this.kB)};function lE(){throw"cannot construct a btPrimitiveManagerBase, no constructor in IDL";}lE.prototype=Object.create(f.prototype);lE.prototype.constructor=lE;lE.prototype.lB=lE;lE.mB={};b.btPrimitiveManagerBase=lE;lE.prototype.is_trimesh=function(){return!!ue(this.kB)};lE.prototype.get_primitive_count=function(){return ve(this.kB)}; +lE.prototype.get_primitive_box=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);we(d,a,c)};lE.prototype.get_primitive_triangle=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);xe(d,a,c)};lE.prototype.__destroy__=function(){ye(this.kB)};function w(){throw"cannot construct a btGImpactShapeInterface, no constructor in IDL";}w.prototype=Object.create(ZD.prototype);w.prototype.constructor=w;w.prototype.lB=w; +w.mB={};b.btGImpactShapeInterface=w;w.prototype.updateBound=function(){ze(this.kB)};w.prototype.postUpdate=function(){Ae(this.kB)};w.prototype.getShapeType=function(){return Be(this.kB)};w.prototype.getName=function(){return sa(Ce(this.kB))};w.prototype.getGImpactShapeType=function(){return De(this.kB)};w.prototype.getPrimitiveManager=function(){return k(Ee(this.kB),lE)};w.prototype.getNumChildShapes=function(){return Fe(this.kB)};w.prototype.childrenHasTransform=function(){return!!Ge(this.kB)}; +w.prototype.needsRetrieveTriangles=function(){return!!He(this.kB)};w.prototype.needsRetrieveTetrahedrons=function(){return!!Ie(this.kB)};w.prototype.getBulletTriangle=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Je(d,a,c)};w.prototype.getBulletTetrahedron=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Ke(d,a,c)}; +w.prototype.getChildShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Le(c,a),l)};w.prototype.getChildTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Me(c,a),r)};w.prototype.setChildTransform=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Ne(d,a,c)};w.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Oe(c,a)}; +w.prototype.getLocalScaling=function(){return k(Pe(this.kB),m)};w.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Qe(d,a,c)};w.prototype.__destroy__=function(){Re(this.kB)};function mE(){throw"cannot construct a btActivatingCollisionAlgorithm, no constructor in IDL";}mE.prototype=Object.create($D.prototype);mE.prototype.constructor=mE;mE.prototype.lB=mE;mE.mB={};b.btActivatingCollisionAlgorithm=mE; +mE.prototype.__destroy__=function(){Se(this.kB)};function nE(a){a&&"object"===typeof a&&(a=a.kB);this.kB=void 0===a?Te():Ue(a);h(nE)[this.kB]=this}nE.prototype=Object.create(f.prototype);nE.prototype.constructor=nE;nE.prototype.lB=nE;nE.mB={};b.btDefaultCollisionConfiguration=nE;nE.prototype.__destroy__=function(){Ve(this.kB)};function TD(){throw"cannot construct a btDispatcher, no constructor in IDL";}TD.prototype=Object.create(f.prototype);TD.prototype.constructor=TD;TD.prototype.lB=TD;TD.mB={}; +b.btDispatcher=TD;TD.prototype.getNumManifolds=function(){return We(this.kB)};TD.prototype.getManifoldByIndexInternal=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Xe(c,a),oE)};TD.prototype.__destroy__=function(){Ye(this.kB)}; +function pE(a,c,d,e,g){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);this.kB=void 0===e?Ze(a,c,d):void 0===g?_emscripten_bind_btGeneric6DofConstraint_btGeneric6DofConstraint_4(a,c,d,e):$e(a,c,d,e,g);h(pE)[this.kB]=this}pE.prototype=Object.create(aE.prototype);pE.prototype.constructor=pE;pE.prototype.lB=pE;pE.mB={};b.btGeneric6DofConstraint=pE; +pE.prototype.setLinearLowerLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);af(c,a)};pE.prototype.setLinearUpperLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);bf(c,a)};pE.prototype.setAngularLowerLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);cf(c,a)};pE.prototype.setAngularUpperLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);df(c,a)};pE.prototype.getFrameOffsetA=function(){return k(ef(this.kB),r)}; +pE.prototype.enableFeedback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ff(c,a)};pE.prototype.getBreakingImpulseThreshold=function(){return gf(this.kB)};pE.prototype.setBreakingImpulseThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hf(c,a)};pE.prototype.getParam=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return jf(d,a,c)}; +pE.prototype.setParam=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);kf(e,a,c,d)};pE.prototype.__destroy__=function(){lf(this.kB)};function x(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=mf(a,c,d,e);h(x)[this.kB]=this}x.prototype=Object.create(bE.prototype);x.prototype.constructor=x;x.prototype.lB=x;x.mB={}; +b.btDiscreteDynamicsWorld=x;x.prototype.setGravity=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);nf(c,a)};x.prototype.getGravity=function(){return k(of(this.kB),m)};x.prototype.addRigidBody=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?pf(e,a):void 0===d?_emscripten_bind_btDiscreteDynamicsWorld_addRigidBody_2(e,a,c):qf(e,a,c,d)}; +x.prototype.removeRigidBody=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);rf(c,a)};x.prototype.addConstraint=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);void 0===c?sf(d,a):tf(d,a,c)};x.prototype.removeConstraint=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);uf(c,a)}; +x.prototype.stepSimulation=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);return void 0===c?vf(e,a):void 0===d?wf(e,a,c):xf(e,a,c,d)};x.prototype.setContactAddedCallback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yf(c,a)};x.prototype.setContactProcessedCallback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zf(c,a)}; +x.prototype.setContactDestroyedCallback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Af(c,a)};x.prototype.getDispatcher=function(){return k(Bf(this.kB),TD)};x.prototype.rayTest=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Cf(e,a,c,d)};x.prototype.getPairCache=function(){return k(Df(this.kB),UD)};x.prototype.getDispatchInfo=function(){return k(Ef(this.kB),p)}; +x.prototype.addCollisionObject=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?Ff(e,a):void 0===d?Gf(e,a,c):Hf(e,a,c,d)};x.prototype.removeCollisionObject=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);If(c,a)};x.prototype.getBroadphase=function(){return k(Jf(this.kB),VD)}; +x.prototype.convexSweepTest=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);Kf(n,a,c,d,e,g)};x.prototype.contactPairTest=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Lf(e,a,c,d)}; +x.prototype.contactTest=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Mf(d,a,c)};x.prototype.updateSingleAabb=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Nf(c,a)};x.prototype.setDebugDrawer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Of(c,a)};x.prototype.getDebugDrawer=function(){return k(Pf(this.kB),WD)};x.prototype.debugDrawWorld=function(){Qf(this.kB)}; +x.prototype.debugDrawObject=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Rf(e,a,c,d)};x.prototype.addAction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Sf(c,a)};x.prototype.removeAction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Tf(c,a)};x.prototype.getSolverInfo=function(){return k(Uf(this.kB),t)}; +x.prototype.setInternalTickCallback=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?Vf(e,a):void 0===d?Wf(e,a,c):Xf(e,a,c,d)};x.prototype.__destroy__=function(){Yf(this.kB)};function qE(){throw"cannot construct a btVehicleRaycaster, no constructor in IDL";}qE.prototype=Object.create(f.prototype);qE.prototype.constructor=qE;qE.prototype.lB=qE;qE.mB={};b.btVehicleRaycaster=qE; +qE.prototype.castRay=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Zf(e,a,c,d)};qE.prototype.__destroy__=function(){$f(this.kB)};function rE(){throw"cannot construct a btActionInterface, no constructor in IDL";}rE.prototype=Object.create(f.prototype);rE.prototype.constructor=rE;rE.prototype.lB=rE;rE.mB={};b.btActionInterface=rE; +rE.prototype.updateAction=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);ag(d,a,c)};rE.prototype.__destroy__=function(){bg(this.kB)};function y(){this.kB=cg();h(y)[this.kB]=this}y.prototype=Object.create(q.prototype);y.prototype.constructor=y;y.prototype.lB=y;y.mB={};b.btGhostObject=y;y.prototype.getNumOverlappingObjects=function(){return dg(this.kB)}; +y.prototype.getOverlappingObject=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(eg(c,a),q)};y.prototype.setAnisotropicFriction=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);fg(d,a,c)};y.prototype.getCollisionShape=function(){return k(gg(this.kB),l)};y.prototype.setContactProcessingThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hg(c,a)}; +y.prototype.setActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ig(c,a)};y.prototype.forceActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jg(c,a)};y.prototype.activate=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);void 0===a?kg(c):lg(c,a)};y.prototype.isActive=function(){return!!mg(this.kB)};y.prototype.isKinematicObject=function(){return!!ng(this.kB)};y.prototype.isStaticObject=function(){return!!og(this.kB)}; +y.prototype.isStaticOrKinematicObject=function(){return!!pg(this.kB)};y.prototype.getRestitution=function(){return qg(this.kB)};y.prototype.getFriction=function(){return rg(this.kB)};y.prototype.getRollingFriction=function(){return sg(this.kB)};y.prototype.setRestitution=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);tg(c,a)};y.prototype.setFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ug(c,a)}; +y.prototype.setRollingFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vg(c,a)};y.prototype.getWorldTransform=function(){return k(wg(this.kB),r)};y.prototype.getCollisionFlags=function(){return xg(this.kB)};y.prototype.setCollisionFlags=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yg(c,a)};y.prototype.setWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zg(c,a)}; +y.prototype.setCollisionShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ag(c,a)};y.prototype.setCcdMotionThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Bg(c,a)};y.prototype.setCcdSweptSphereRadius=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Cg(c,a)};y.prototype.getUserIndex=function(){return Dg(this.kB)};y.prototype.setUserIndex=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Eg(c,a)}; +y.prototype.getUserPointer=function(){return k(Fg(this.kB),XD)};y.prototype.setUserPointer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Gg(c,a)};y.prototype.getBroadphaseHandle=function(){return k(Hg(this.kB),YD)};y.prototype.__destroy__=function(){Ig(this.kB)};function sE(){throw"cannot construct a btSoftBodySolver, no constructor in IDL";}sE.prototype=Object.create(f.prototype);sE.prototype.constructor=sE;sE.prototype.lB=sE;sE.mB={};b.btSoftBodySolver=sE;sE.prototype.__destroy__=function(){Jg(this.kB)}; +function XD(){throw"cannot construct a VoidPtr, no constructor in IDL";}XD.prototype=Object.create(f.prototype);XD.prototype.constructor=XD;XD.prototype.lB=XD;XD.mB={};b.VoidPtr=XD;XD.prototype.__destroy__=function(){Kg(this.kB)};function tE(){this.kB=Lg();h(tE)[this.kB]=this}tE.prototype=Object.create(WD.prototype);tE.prototype.constructor=tE;tE.prototype.lB=tE;tE.mB={};b.DebugDrawer=tE; +tE.prototype.drawLine=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Mg(e,a,c,d)};tE.prototype.drawContactPoint=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);Ng(n,a,c,d,e,g)}; +tE.prototype.reportErrorWarning=function(a){var c=this.kB;ND();a=a&&"object"===typeof a?a.kB:QD(a);Og(c,a)};tE.prototype.draw3dText=function(a,c){var d=this.kB;ND();a&&"object"===typeof a&&(a=a.kB);c=c&&"object"===typeof c?c.kB:QD(c);Pg(d,a,c)};tE.prototype.setDebugMode=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Qg(c,a)};tE.prototype.getDebugMode=function(){return Rg(this.kB)};tE.prototype.__destroy__=function(){Sg(this.kB)}; +function A(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=void 0===a?Tg():void 0===c?_emscripten_bind_btVector4_btVector4_1(a):void 0===d?_emscripten_bind_btVector4_btVector4_2(a,c):void 0===e?_emscripten_bind_btVector4_btVector4_3(a,c,d):Ug(a,c,d,e);h(A)[this.kB]=this}A.prototype=Object.create(m.prototype);A.prototype.constructor=A;A.prototype.lB=A;A.mB={};b.btVector4=A; +A.prototype.w=A.prototype.w=function(){return Vg(this.kB)};A.prototype.setValue=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);Wg(g,a,c,d,e)};A.prototype.length=A.prototype.length=function(){return Xg(this.kB)};A.prototype.x=A.prototype.x=function(){return Yg(this.kB)};A.prototype.y=A.prototype.y=function(){return Zg(this.kB)};A.prototype.z=A.prototype.z=function(){return $g(this.kB)}; +A.prototype.setX=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ah(c,a)};A.prototype.setY=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);bh(c,a)};A.prototype.setZ=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ch(c,a)};A.prototype.normalize=A.prototype.normalize=function(){dh(this.kB)};A.prototype.rotate=A.prototype.rotate=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return k(eh(d,a,c),m)}; +A.prototype.dot=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return fh(c,a)};A.prototype.op_mul=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(gh(c,a),m)};A.prototype.op_add=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(hh(c,a),m)};A.prototype.op_sub=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(ih(c,a),m)};A.prototype.__destroy__=function(){jh(this.kB)}; +function B(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=kh(a,c,d,e);h(B)[this.kB]=this}B.prototype=Object.create(cE.prototype);B.prototype.constructor=B;B.prototype.lB=B;B.mB={};b.btQuaternion=B;B.prototype.setValue=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);lh(g,a,c,d,e)}; +B.prototype.setEulerZYX=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);mh(e,a,c,d)};B.prototype.setRotation=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);nh(d,a,c)};B.prototype.normalize=B.prototype.normalize=function(){oh(this.kB)};B.prototype.length2=function(){return ph(this.kB)};B.prototype.length=B.prototype.length=function(){return qh(this.kB)}; +B.prototype.dot=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return rh(c,a)};B.prototype.normalized=function(){return k(sh(this.kB),B)};B.prototype.getAxis=function(){return k(th(this.kB),m)};B.prototype.inverse=B.prototype.inverse=function(){return k(uh(this.kB),B)};B.prototype.getAngle=function(){return vh(this.kB)};B.prototype.getAngleShortestPath=function(){return wh(this.kB)}; +B.prototype.angle=B.prototype.angle=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return xh(c,a)};B.prototype.angleShortestPath=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return yh(c,a)};B.prototype.op_add=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(zh(c,a),B)};B.prototype.op_sub=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Ah(c,a),B)}; +B.prototype.op_mul=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Bh(c,a),B)};B.prototype.op_mulq=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Ch(c,a),B)};B.prototype.op_div=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Dh(c,a),B)};B.prototype.x=B.prototype.x=function(){return Eh(this.kB)};B.prototype.y=B.prototype.y=function(){return Fh(this.kB)};B.prototype.z=B.prototype.z=function(){return Gh(this.kB)}; +B.prototype.w=B.prototype.w=function(){return Hh(this.kB)};B.prototype.setX=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ih(c,a)};B.prototype.setY=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jh(c,a)};B.prototype.setZ=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Kh(c,a)};B.prototype.setW=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Lh(c,a)};B.prototype.__destroy__=function(){Mh(this.kB)}; +function uE(){throw"cannot construct a btMatrix3x3, no constructor in IDL";}uE.prototype=Object.create(f.prototype);uE.prototype.constructor=uE;uE.prototype.lB=uE;uE.mB={};b.btMatrix3x3=uE;uE.prototype.setEulerZYX=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Nh(e,a,c,d)};uE.prototype.getRotation=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Oh(c,a)}; +uE.prototype.getRow=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Ph(c,a),m)};uE.prototype.__destroy__=function(){Qh(this.kB)};function r(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=void 0===a?Rh():void 0===c?_emscripten_bind_btTransform_btTransform_1(a):Sh(a,c);h(r)[this.kB]=this}r.prototype=Object.create(f.prototype);r.prototype.constructor=r;r.prototype.lB=r;r.mB={};b.btTransform=r;r.prototype.setIdentity=function(){Th(this.kB)}; +r.prototype.setOrigin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Uh(c,a)};r.prototype.setRotation=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vh(c,a)};r.prototype.getOrigin=function(){return k(Wh(this.kB),m)};r.prototype.getRotation=function(){return k(Xh(this.kB),B)};r.prototype.getBasis=function(){return k(Yh(this.kB),uE)};r.prototype.setFromOpenGLMatrix=function(a){var c=this.kB;ND();"object"==typeof a&&(a=RD(a));Zh(c,a)}; +r.prototype.inverse=r.prototype.inverse=function(){return k($h(this.kB),r)};r.prototype.op_mul=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(ai(c,a),r)};r.prototype.__destroy__=function(){bi(this.kB)};function vE(){this.kB=ci();h(vE)[this.kB]=this}vE.prototype=Object.create(dE.prototype);vE.prototype.constructor=vE;vE.prototype.lB=vE;vE.mB={};b.MotionState=vE;vE.prototype.getWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);di(c,a)}; +vE.prototype.setWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ei(c,a)};vE.prototype.__destroy__=function(){fi(this.kB)};function wE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=void 0===a?gi():void 0===c?hi(a):ii(a,c);h(wE)[this.kB]=this}wE.prototype=Object.create(dE.prototype);wE.prototype.constructor=wE;wE.prototype.lB=wE;wE.mB={};b.btDefaultMotionState=wE; +wE.prototype.getWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ji(c,a)};wE.prototype.setWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ki(c,a)};wE.prototype.get_m_graphicsWorldTrans=wE.prototype.lD=function(){return k(li(this.kB),r)};wE.prototype.set_m_graphicsWorldTrans=wE.prototype.cG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);mi(c,a)};Object.defineProperty(wE.prototype,"m_graphicsWorldTrans",{get:wE.prototype.lD,set:wE.prototype.cG}); +wE.prototype.__destroy__=function(){ni(this.kB)};function xE(){throw"cannot construct a btCollisionObjectWrapper, no constructor in IDL";}xE.prototype=Object.create(f.prototype);xE.prototype.constructor=xE;xE.prototype.lB=xE;xE.mB={};b.btCollisionObjectWrapper=xE;xE.prototype.getWorldTransform=function(){return k(oi(this.kB),r)};xE.prototype.getCollisionObject=function(){return k(pi(this.kB),q)};xE.prototype.getCollisionShape=function(){return k(qi(this.kB),l)}; +function C(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=ri(a,c);h(C)[this.kB]=this}C.prototype=Object.create(u.prototype);C.prototype.constructor=C;C.prototype.lB=C;C.mB={};b.ClosestRayResultCallback=C;C.prototype.hasHit=function(){return!!si(this.kB)};C.prototype.get_m_rayFromWorld=C.prototype.NB=function(){return k(ti(this.kB),m)};C.prototype.set_m_rayFromWorld=C.prototype.XB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ui(c,a)}; +Object.defineProperty(C.prototype,"m_rayFromWorld",{get:C.prototype.NB,set:C.prototype.XB});C.prototype.get_m_rayToWorld=C.prototype.OB=function(){return k(vi(this.kB),m)};C.prototype.set_m_rayToWorld=C.prototype.YB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);wi(c,a)};Object.defineProperty(C.prototype,"m_rayToWorld",{get:C.prototype.OB,set:C.prototype.YB});C.prototype.get_m_hitNormalWorld=C.prototype.xB=function(){return k(xi(this.kB),m)}; +C.prototype.set_m_hitNormalWorld=C.prototype.EB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yi(c,a)};Object.defineProperty(C.prototype,"m_hitNormalWorld",{get:C.prototype.xB,set:C.prototype.EB});C.prototype.get_m_hitPointWorld=C.prototype.yB=function(){return k(zi(this.kB),m)};C.prototype.set_m_hitPointWorld=C.prototype.FB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ai(c,a)};Object.defineProperty(C.prototype,"m_hitPointWorld",{get:C.prototype.yB,set:C.prototype.FB}); +C.prototype.get_m_collisionFilterGroup=C.prototype.nB=function(){return Bi(this.kB)};C.prototype.set_m_collisionFilterGroup=C.prototype.pB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ci(c,a)};Object.defineProperty(C.prototype,"m_collisionFilterGroup",{get:C.prototype.nB,set:C.prototype.pB});C.prototype.get_m_collisionFilterMask=C.prototype.oB=function(){return Di(this.kB)}; +C.prototype.set_m_collisionFilterMask=C.prototype.qB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ei(c,a)};Object.defineProperty(C.prototype,"m_collisionFilterMask",{get:C.prototype.oB,set:C.prototype.qB});C.prototype.get_m_closestHitFraction=C.prototype.rB=function(){return Fi(this.kB)};C.prototype.set_m_closestHitFraction=C.prototype.sB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Gi(c,a)};Object.defineProperty(C.prototype,"m_closestHitFraction",{get:C.prototype.rB,set:C.prototype.sB}); +C.prototype.get_m_collisionObject=C.prototype.vB=function(){return k(Hi(this.kB),q)};C.prototype.set_m_collisionObject=C.prototype.CB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ii(c,a)};Object.defineProperty(C.prototype,"m_collisionObject",{get:C.prototype.vB,set:C.prototype.CB});C.prototype.get_m_flags=C.prototype.tB=function(){return Ji(this.kB)};C.prototype.set_m_flags=C.prototype.uB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ki(c,a)}; +Object.defineProperty(C.prototype,"m_flags",{get:C.prototype.tB,set:C.prototype.uB});C.prototype.__destroy__=function(){Li(this.kB)};function yE(){throw"cannot construct a btConstCollisionObjectArray, no constructor in IDL";}yE.prototype=Object.create(f.prototype);yE.prototype.constructor=yE;yE.prototype.lB=yE;yE.mB={};b.btConstCollisionObjectArray=yE;yE.prototype.size=yE.prototype.size=function(){return Mi(this.kB)}; +yE.prototype.at=yE.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Ni(c,a),q)};yE.prototype.__destroy__=function(){Oi(this.kB)};function zE(){throw"cannot construct a btScalarArray, no constructor in IDL";}zE.prototype=Object.create(f.prototype);zE.prototype.constructor=zE;zE.prototype.lB=zE;zE.mB={};b.btScalarArray=zE;zE.prototype.size=zE.prototype.size=function(){return Pi(this.kB)}; +zE.prototype.at=zE.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return Qi(c,a)};zE.prototype.__destroy__=function(){Ri(this.kB)};function D(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=Si(a,c);h(D)[this.kB]=this}D.prototype=Object.create(u.prototype);D.prototype.constructor=D;D.prototype.lB=D;D.mB={};b.AllHitsRayResultCallback=D;D.prototype.hasHit=function(){return!!Ti(this.kB)}; +D.prototype.get_m_collisionObjects=D.prototype.UC=function(){return k(Ui(this.kB),yE)};D.prototype.set_m_collisionObjects=D.prototype.LF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vi(c,a)};Object.defineProperty(D.prototype,"m_collisionObjects",{get:D.prototype.UC,set:D.prototype.LF});D.prototype.get_m_rayFromWorld=D.prototype.NB=function(){return k(Wi(this.kB),m)};D.prototype.set_m_rayFromWorld=D.prototype.XB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Xi(c,a)}; +Object.defineProperty(D.prototype,"m_rayFromWorld",{get:D.prototype.NB,set:D.prototype.XB});D.prototype.get_m_rayToWorld=D.prototype.OB=function(){return k(Yi(this.kB),m)};D.prototype.set_m_rayToWorld=D.prototype.YB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Zi(c,a)};Object.defineProperty(D.prototype,"m_rayToWorld",{get:D.prototype.OB,set:D.prototype.YB});D.prototype.get_m_hitNormalWorld=D.prototype.xB=function(){return k($i(this.kB),AE)}; +D.prototype.set_m_hitNormalWorld=D.prototype.EB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);aj(c,a)};Object.defineProperty(D.prototype,"m_hitNormalWorld",{get:D.prototype.xB,set:D.prototype.EB});D.prototype.get_m_hitPointWorld=D.prototype.yB=function(){return k(bj(this.kB),AE)};D.prototype.set_m_hitPointWorld=D.prototype.FB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);cj(c,a)};Object.defineProperty(D.prototype,"m_hitPointWorld",{get:D.prototype.yB,set:D.prototype.FB}); +D.prototype.get_m_hitFractions=D.prototype.qD=function(){return k(dj(this.kB),zE)};D.prototype.set_m_hitFractions=D.prototype.hG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ej(c,a)};Object.defineProperty(D.prototype,"m_hitFractions",{get:D.prototype.qD,set:D.prototype.hG});D.prototype.get_m_collisionFilterGroup=D.prototype.nB=function(){return fj(this.kB)};D.prototype.set_m_collisionFilterGroup=D.prototype.pB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);gj(c,a)}; +Object.defineProperty(D.prototype,"m_collisionFilterGroup",{get:D.prototype.nB,set:D.prototype.pB});D.prototype.get_m_collisionFilterMask=D.prototype.oB=function(){return hj(this.kB)};D.prototype.set_m_collisionFilterMask=D.prototype.qB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ij(c,a)};Object.defineProperty(D.prototype,"m_collisionFilterMask",{get:D.prototype.oB,set:D.prototype.qB});D.prototype.get_m_closestHitFraction=D.prototype.rB=function(){return jj(this.kB)}; +D.prototype.set_m_closestHitFraction=D.prototype.sB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);kj(c,a)};Object.defineProperty(D.prototype,"m_closestHitFraction",{get:D.prototype.rB,set:D.prototype.sB});D.prototype.get_m_collisionObject=D.prototype.vB=function(){return k(lj(this.kB),q)};D.prototype.set_m_collisionObject=D.prototype.CB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);mj(c,a)};Object.defineProperty(D.prototype,"m_collisionObject",{get:D.prototype.vB,set:D.prototype.CB}); +D.prototype.get_m_flags=D.prototype.tB=function(){return nj(this.kB)};D.prototype.set_m_flags=D.prototype.uB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);oj(c,a)};Object.defineProperty(D.prototype,"m_flags",{get:D.prototype.tB,set:D.prototype.uB});D.prototype.__destroy__=function(){pj(this.kB)};function E(){throw"cannot construct a btManifoldPoint, no constructor in IDL";}E.prototype=Object.create(f.prototype);E.prototype.constructor=E;E.prototype.lB=E;E.mB={};b.btManifoldPoint=E; +E.prototype.getPositionWorldOnA=function(){return k(qj(this.kB),m)};E.prototype.getPositionWorldOnB=function(){return k(rj(this.kB),m)};E.prototype.getAppliedImpulse=function(){return sj(this.kB)};E.prototype.getDistance=function(){return tj(this.kB)};E.prototype.get_m_localPointA=E.prototype.GD=function(){return k(uj(this.kB),m)};E.prototype.set_m_localPointA=E.prototype.xG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vj(c,a)}; +Object.defineProperty(E.prototype,"m_localPointA",{get:E.prototype.GD,set:E.prototype.xG});E.prototype.get_m_localPointB=E.prototype.HD=function(){return k(wj(this.kB),m)};E.prototype.set_m_localPointB=E.prototype.yG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xj(c,a)};Object.defineProperty(E.prototype,"m_localPointB",{get:E.prototype.HD,set:E.prototype.yG});E.prototype.get_m_positionWorldOnB=E.prototype.YD=function(){return k(yj(this.kB),m)}; +E.prototype.set_m_positionWorldOnB=E.prototype.PG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zj(c,a)};Object.defineProperty(E.prototype,"m_positionWorldOnB",{get:E.prototype.YD,set:E.prototype.PG});E.prototype.get_m_positionWorldOnA=E.prototype.XD=function(){return k(Aj(this.kB),m)};E.prototype.set_m_positionWorldOnA=E.prototype.OG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Bj(c,a)};Object.defineProperty(E.prototype,"m_positionWorldOnA",{get:E.prototype.XD,set:E.prototype.OG}); +E.prototype.get_m_normalWorldOnB=E.prototype.SD=function(){return k(Cj(this.kB),m)};E.prototype.set_m_normalWorldOnB=E.prototype.JG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dj(c,a)};Object.defineProperty(E.prototype,"m_normalWorldOnB",{get:E.prototype.SD,set:E.prototype.JG});E.prototype.get_m_userPersistentData=E.prototype.zE=function(){return Ej(this.kB)};E.prototype.set_m_userPersistentData=E.prototype.rH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Fj(c,a)}; +Object.defineProperty(E.prototype,"m_userPersistentData",{get:E.prototype.zE,set:E.prototype.rH});E.prototype.__destroy__=function(){Gj(this.kB)};function BE(){this.kB=Hj();h(BE)[this.kB]=this}BE.prototype=Object.create(eE.prototype);BE.prototype.constructor=BE;BE.prototype.lB=BE;BE.mB={};b.ConcreteContactResultCallback=BE; +BE.prototype.addSingleResult=function(a,c,d,e,g,n,z){var T=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);n&&"object"===typeof n&&(n=n.kB);z&&"object"===typeof z&&(z=z.kB);return Ij(T,a,c,d,e,g,n,z)};BE.prototype.__destroy__=function(){Jj(this.kB)};function CE(){throw"cannot construct a LocalShapeInfo, no constructor in IDL";}CE.prototype=Object.create(f.prototype); +CE.prototype.constructor=CE;CE.prototype.lB=CE;CE.mB={};b.LocalShapeInfo=CE;CE.prototype.get_m_shapePart=CE.prototype.gE=function(){return Kj(this.kB)};CE.prototype.set_m_shapePart=CE.prototype.ZG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Lj(c,a)};Object.defineProperty(CE.prototype,"m_shapePart",{get:CE.prototype.gE,set:CE.prototype.ZG});CE.prototype.get_m_triangleIndex=CE.prototype.vE=function(){return Mj(this.kB)}; +CE.prototype.set_m_triangleIndex=CE.prototype.nH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Nj(c,a)};Object.defineProperty(CE.prototype,"m_triangleIndex",{get:CE.prototype.vE,set:CE.prototype.nH});CE.prototype.__destroy__=function(){Oj(this.kB)};function F(a,c,d,e,g){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);this.kB=Pj(a,c,d,e,g);h(F)[this.kB]=this} +F.prototype=Object.create(f.prototype);F.prototype.constructor=F;F.prototype.lB=F;F.mB={};b.LocalConvexResult=F;F.prototype.get_m_hitCollisionObject=F.prototype.LB=function(){return k(Qj(this.kB),q)};F.prototype.set_m_hitCollisionObject=F.prototype.VB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Rj(c,a)};Object.defineProperty(F.prototype,"m_hitCollisionObject",{get:F.prototype.LB,set:F.prototype.VB});F.prototype.get_m_localShapeInfo=F.prototype.ID=function(){return k(Sj(this.kB),CE)}; +F.prototype.set_m_localShapeInfo=F.prototype.zG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Tj(c,a)};Object.defineProperty(F.prototype,"m_localShapeInfo",{get:F.prototype.ID,set:F.prototype.zG});F.prototype.get_m_hitNormalLocal=F.prototype.sD=function(){return k(Uj(this.kB),m)};F.prototype.set_m_hitNormalLocal=F.prototype.jG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vj(c,a)};Object.defineProperty(F.prototype,"m_hitNormalLocal",{get:F.prototype.sD,set:F.prototype.jG}); +F.prototype.get_m_hitPointLocal=F.prototype.uD=function(){return k(Wj(this.kB),m)};F.prototype.set_m_hitPointLocal=F.prototype.lG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Xj(c,a)};Object.defineProperty(F.prototype,"m_hitPointLocal",{get:F.prototype.uD,set:F.prototype.lG});F.prototype.get_m_hitFraction=F.prototype.pD=function(){return Yj(this.kB)};F.prototype.set_m_hitFraction=F.prototype.gG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Zj(c,a)}; +Object.defineProperty(F.prototype,"m_hitFraction",{get:F.prototype.pD,set:F.prototype.gG});F.prototype.__destroy__=function(){ak(this.kB)};function G(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=bk(a,c);h(G)[this.kB]=this}G.prototype=Object.create(v.prototype);G.prototype.constructor=G;G.prototype.lB=G;G.mB={};b.ClosestConvexResultCallback=G;G.prototype.hasHit=function(){return!!ck(this.kB)}; +G.prototype.get_m_hitCollisionObject=G.prototype.LB=function(){return k(dk(this.kB),q)};G.prototype.set_m_hitCollisionObject=G.prototype.VB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ek(c,a)};Object.defineProperty(G.prototype,"m_hitCollisionObject",{get:G.prototype.LB,set:G.prototype.VB});G.prototype.get_m_convexFromWorld=G.prototype.ZC=function(){return k(fk(this.kB),m)}; +G.prototype.set_m_convexFromWorld=G.prototype.QF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);gk(c,a)};Object.defineProperty(G.prototype,"m_convexFromWorld",{get:G.prototype.ZC,set:G.prototype.QF});G.prototype.get_m_convexToWorld=G.prototype.$C=function(){return k(hk(this.kB),m)};G.prototype.set_m_convexToWorld=G.prototype.RF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ik(c,a)};Object.defineProperty(G.prototype,"m_convexToWorld",{get:G.prototype.$C,set:G.prototype.RF}); +G.prototype.get_m_hitNormalWorld=G.prototype.xB=function(){return k(jk(this.kB),m)};G.prototype.set_m_hitNormalWorld=G.prototype.EB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);kk(c,a)};Object.defineProperty(G.prototype,"m_hitNormalWorld",{get:G.prototype.xB,set:G.prototype.EB});G.prototype.get_m_hitPointWorld=G.prototype.yB=function(){return k(lk(this.kB),m)};G.prototype.set_m_hitPointWorld=G.prototype.FB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);mk(c,a)}; +Object.defineProperty(G.prototype,"m_hitPointWorld",{get:G.prototype.yB,set:G.prototype.FB});G.prototype.get_m_collisionFilterGroup=G.prototype.nB=function(){return nk(this.kB)};G.prototype.set_m_collisionFilterGroup=G.prototype.pB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ok(c,a)};Object.defineProperty(G.prototype,"m_collisionFilterGroup",{get:G.prototype.nB,set:G.prototype.pB});G.prototype.get_m_collisionFilterMask=G.prototype.oB=function(){return pk(this.kB)}; +G.prototype.set_m_collisionFilterMask=G.prototype.qB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);qk(c,a)};Object.defineProperty(G.prototype,"m_collisionFilterMask",{get:G.prototype.oB,set:G.prototype.qB});G.prototype.get_m_closestHitFraction=G.prototype.rB=function(){return rk(this.kB)};G.prototype.set_m_closestHitFraction=G.prototype.sB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);sk(c,a)};Object.defineProperty(G.prototype,"m_closestHitFraction",{get:G.prototype.rB,set:G.prototype.sB}); +G.prototype.__destroy__=function(){tk(this.kB)};function DE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=void 0===c?uk(a):vk(a,c);h(DE)[this.kB]=this}DE.prototype=Object.create(fE.prototype);DE.prototype.constructor=DE;DE.prototype.lB=DE;DE.mB={};b.btConvexTriangleMeshShape=DE;DE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);wk(c,a)};DE.prototype.getLocalScaling=function(){return k(xk(this.kB),m)}; +DE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);yk(d,a,c)};DE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zk(c,a)};DE.prototype.getMargin=function(){return Ak(this.kB)};DE.prototype.__destroy__=function(){Bk(this.kB)};function EE(a){a&&"object"===typeof a&&(a=a.kB);this.kB=Ck(a);h(EE)[this.kB]=this}EE.prototype=Object.create(l.prototype);EE.prototype.constructor=EE;EE.prototype.lB=EE; +EE.mB={};b.btBoxShape=EE;EE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dk(c,a)};EE.prototype.getMargin=function(){return Ek(this.kB)};EE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Fk(c,a)};EE.prototype.getLocalScaling=function(){return k(Gk(this.kB),m)};EE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Hk(d,a,c)};EE.prototype.__destroy__=function(){Ik(this.kB)}; +function FE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=Jk(a,c);h(FE)[this.kB]=this}FE.prototype=Object.create(gE.prototype);FE.prototype.constructor=FE;FE.prototype.lB=FE;FE.mB={};b.btCapsuleShapeX=FE;FE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Kk(c,a)};FE.prototype.getMargin=function(){return Lk(this.kB)};FE.prototype.getUpAxis=function(){return Mk(this.kB)};FE.prototype.getRadius=function(){return Nk(this.kB)}; +FE.prototype.getHalfHeight=function(){return Ok(this.kB)};FE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Pk(c,a)};FE.prototype.getLocalScaling=function(){return k(Qk(this.kB),m)};FE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Rk(d,a,c)};FE.prototype.__destroy__=function(){Sk(this.kB)}; +function GE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=Tk(a,c);h(GE)[this.kB]=this}GE.prototype=Object.create(gE.prototype);GE.prototype.constructor=GE;GE.prototype.lB=GE;GE.mB={};b.btCapsuleShapeZ=GE;GE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Uk(c,a)};GE.prototype.getMargin=function(){return Vk(this.kB)};GE.prototype.getUpAxis=function(){return Wk(this.kB)};GE.prototype.getRadius=function(){return Xk(this.kB)}; +GE.prototype.getHalfHeight=function(){return Yk(this.kB)};GE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Zk(c,a)};GE.prototype.getLocalScaling=function(){return k($k(this.kB),m)};GE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);al(d,a,c)};GE.prototype.__destroy__=function(){bl(this.kB)};function HE(a){a&&"object"===typeof a&&(a=a.kB);this.kB=cl(a);h(HE)[this.kB]=this} +HE.prototype=Object.create(hE.prototype);HE.prototype.constructor=HE;HE.prototype.lB=HE;HE.mB={};b.btCylinderShapeX=HE;HE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);dl(c,a)};HE.prototype.getMargin=function(){return el(this.kB)};HE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fl(c,a)};HE.prototype.getLocalScaling=function(){return k(gl(this.kB),m)}; +HE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);hl(d,a,c)};HE.prototype.__destroy__=function(){il(this.kB)};function IE(a){a&&"object"===typeof a&&(a=a.kB);this.kB=jl(a);h(IE)[this.kB]=this}IE.prototype=Object.create(hE.prototype);IE.prototype.constructor=IE;IE.prototype.lB=IE;IE.mB={};b.btCylinderShapeZ=IE;IE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);kl(c,a)}; +IE.prototype.getMargin=function(){return ll(this.kB)};IE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ml(c,a)};IE.prototype.getLocalScaling=function(){return k(nl(this.kB),m)};IE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);ol(d,a,c)};IE.prototype.__destroy__=function(){pl(this.kB)};function JE(a){a&&"object"===typeof a&&(a=a.kB);this.kB=ql(a);h(JE)[this.kB]=this}JE.prototype=Object.create(l.prototype); +JE.prototype.constructor=JE;JE.prototype.lB=JE;JE.mB={};b.btSphereShape=JE;JE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);rl(c,a)};JE.prototype.getMargin=function(){return sl(this.kB)};JE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);tl(c,a)};JE.prototype.getLocalScaling=function(){return k(ul(this.kB),m)}; +JE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);vl(d,a,c)};JE.prototype.__destroy__=function(){wl(this.kB)};function KE(a,c,d){ND();a&&"object"===typeof a&&(a=a.kB);"object"==typeof c&&(c=RD(c));d&&"object"===typeof d&&(d=d.kB);this.kB=xl(a,c,d);h(KE)[this.kB]=this}KE.prototype=Object.create(l.prototype);KE.prototype.constructor=KE;KE.prototype.lB=KE;KE.mB={};b.btMultiSphereShape=KE; +KE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yl(c,a)};KE.prototype.getLocalScaling=function(){return k(zl(this.kB),m)};KE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Al(d,a,c)};KE.prototype.__destroy__=function(){Bl(this.kB)};function LE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=Cl(a,c);h(LE)[this.kB]=this}LE.prototype=Object.create(iE.prototype); +LE.prototype.constructor=LE;LE.prototype.lB=LE;LE.mB={};b.btConeShapeX=LE;LE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dl(c,a)};LE.prototype.getLocalScaling=function(){return k(El(this.kB),m)};LE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Fl(d,a,c)};LE.prototype.__destroy__=function(){Gl(this.kB)}; +function ME(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=Hl(a,c);h(ME)[this.kB]=this}ME.prototype=Object.create(iE.prototype);ME.prototype.constructor=ME;ME.prototype.lB=ME;ME.mB={};b.btConeShapeZ=ME;ME.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Il(c,a)};ME.prototype.getLocalScaling=function(){return k(Jl(this.kB),m)}; +ME.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Kl(d,a,c)};ME.prototype.__destroy__=function(){Ll(this.kB)};function NE(){throw"cannot construct a btIntArray, no constructor in IDL";}NE.prototype=Object.create(f.prototype);NE.prototype.constructor=NE;NE.prototype.lB=NE;NE.mB={};b.btIntArray=NE;NE.prototype.size=NE.prototype.size=function(){return Ml(this.kB)}; +NE.prototype.at=NE.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return Nl(c,a)};NE.prototype.__destroy__=function(){Ol(this.kB)};function OE(){throw"cannot construct a btFace, no constructor in IDL";}OE.prototype=Object.create(f.prototype);OE.prototype.constructor=OE;OE.prototype.lB=OE;OE.mB={};b.btFace=OE;OE.prototype.get_m_indices=OE.prototype.xD=function(){return k(Pl(this.kB),NE)}; +OE.prototype.set_m_indices=OE.prototype.oG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ql(c,a)};Object.defineProperty(OE.prototype,"m_indices",{get:OE.prototype.xD,set:OE.prototype.oG});OE.prototype.get_m_plane=OE.prototype.WD=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return Rl(c,a)};OE.prototype.set_m_plane=OE.prototype.NG=function(a,c){var d=this.kB;ND();a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Sl(d,a,c)}; +Object.defineProperty(OE.prototype,"m_plane",{get:OE.prototype.WD,set:OE.prototype.NG});OE.prototype.__destroy__=function(){Tl(this.kB)};function AE(){throw"cannot construct a btVector3Array, no constructor in IDL";}AE.prototype=Object.create(f.prototype);AE.prototype.constructor=AE;AE.prototype.lB=AE;AE.mB={};b.btVector3Array=AE;AE.prototype.size=AE.prototype.size=function(){return Ul(this.kB)}; +AE.prototype.at=AE.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Vl(c,a),m)};AE.prototype.__destroy__=function(){Wl(this.kB)};function PE(){throw"cannot construct a btFaceArray, no constructor in IDL";}PE.prototype=Object.create(f.prototype);PE.prototype.constructor=PE;PE.prototype.lB=PE;PE.mB={};b.btFaceArray=PE;PE.prototype.size=PE.prototype.size=function(){return Xl(this.kB)}; +PE.prototype.at=PE.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Yl(c,a),OE)};PE.prototype.__destroy__=function(){Zl(this.kB)};function QE(){throw"cannot construct a btConvexPolyhedron, no constructor in IDL";}QE.prototype=Object.create(f.prototype);QE.prototype.constructor=QE;QE.prototype.lB=QE;QE.mB={};b.btConvexPolyhedron=QE;QE.prototype.get_m_vertices=QE.prototype.BE=function(){return k($l(this.kB),AE)}; +QE.prototype.set_m_vertices=QE.prototype.tH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);am(c,a)};Object.defineProperty(QE.prototype,"m_vertices",{get:QE.prototype.BE,set:QE.prototype.tH});QE.prototype.get_m_faces=QE.prototype.KB=function(){return k(bm(this.kB),PE)};QE.prototype.set_m_faces=QE.prototype.UB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);cm(c,a)};Object.defineProperty(QE.prototype,"m_faces",{get:QE.prototype.KB,set:QE.prototype.UB}); +QE.prototype.__destroy__=function(){dm(this.kB)};function RE(a,c){ND();"object"==typeof a&&(a=RD(a));c&&"object"===typeof c&&(c=c.kB);this.kB=void 0===a?em():void 0===c?fm(a):gm(a,c);h(RE)[this.kB]=this}RE.prototype=Object.create(l.prototype);RE.prototype.constructor=RE;RE.prototype.lB=RE;RE.mB={};b.btConvexHullShape=RE;RE.prototype.addPoint=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);void 0===c?hm(d,a):im(d,a,c)}; +RE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jm(c,a)};RE.prototype.getMargin=function(){return km(this.kB)};RE.prototype.getNumVertices=function(){return lm(this.kB)};RE.prototype.initializePolyhedralFeatures=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return!!mm(c,a)};RE.prototype.recalcLocalAabb=function(){nm(this.kB)};RE.prototype.getConvexPolyhedron=function(){return k(om(this.kB),QE)}; +RE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pm(c,a)};RE.prototype.getLocalScaling=function(){return k(qm(this.kB),m)};RE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);rm(d,a,c)};RE.prototype.__destroy__=function(){sm(this.kB)};function SE(a){a&&"object"===typeof a&&(a=a.kB);this.kB=tm(a);h(SE)[this.kB]=this}SE.prototype=Object.create(f.prototype);SE.prototype.constructor=SE; +SE.prototype.lB=SE;SE.mB={};b.btShapeHull=SE;SE.prototype.buildHull=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return!!um(c,a)};SE.prototype.numVertices=function(){return wm(this.kB)};SE.prototype.getVertexPointer=function(){return k(xm(this.kB),m)};SE.prototype.__destroy__=function(){ym(this.kB)};function TE(a){a&&"object"===typeof a&&(a=a.kB);this.kB=void 0===a?zm():Am(a);h(TE)[this.kB]=this}TE.prototype=Object.create(l.prototype);TE.prototype.constructor=TE;TE.prototype.lB=TE; +TE.mB={};b.btCompoundShape=TE;TE.prototype.addChildShape=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Bm(d,a,c)};TE.prototype.removeChildShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Cm(c,a)};TE.prototype.removeChildShapeByIndex=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dm(c,a)};TE.prototype.getNumChildShapes=function(){return Em(this.kB)}; +TE.prototype.getChildShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Fm(c,a),l)};TE.prototype.updateChildTransform=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===d?Gm(e,a,c):Hm(e,a,c,d)};TE.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Im(c,a)};TE.prototype.getMargin=function(){return Jm(this.kB)}; +TE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Km(c,a)};TE.prototype.getLocalScaling=function(){return k(Lm(this.kB),m)};TE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Mm(d,a,c)};TE.prototype.__destroy__=function(){Nm(this.kB)};function UE(){throw"cannot construct a btIndexedMesh, no constructor in IDL";}UE.prototype=Object.create(f.prototype);UE.prototype.constructor=UE; +UE.prototype.lB=UE;UE.mB={};b.btIndexedMesh=UE;UE.prototype.get_m_numTriangles=UE.prototype.UD=function(){return Om(this.kB)};UE.prototype.set_m_numTriangles=UE.prototype.LG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Pm(c,a)};Object.defineProperty(UE.prototype,"m_numTriangles",{get:UE.prototype.UD,set:UE.prototype.LG});UE.prototype.__destroy__=function(){Qm(this.kB)};function VE(){throw"cannot construct a btIndexedMeshArray, no constructor in IDL";}VE.prototype=Object.create(f.prototype); +VE.prototype.constructor=VE;VE.prototype.lB=VE;VE.mB={};b.btIndexedMeshArray=VE;VE.prototype.size=VE.prototype.size=function(){return Rm(this.kB)};VE.prototype.at=VE.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Sm(c,a),UE)};VE.prototype.__destroy__=function(){Tm(this.kB)};function WE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=void 0===a?Um():void 0===c?Vm(a):Wm(a,c);h(WE)[this.kB]=this}WE.prototype=Object.create(jE.prototype); +WE.prototype.constructor=WE;WE.prototype.lB=WE;WE.mB={};b.btTriangleMesh=WE;WE.prototype.addTriangle=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);void 0===e?Xm(g,a,c,d):Ym(g,a,c,d,e)};WE.prototype.findOrAddVertex=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return Zm(d,a,c)}; +WE.prototype.addIndex=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$m(c,a)};WE.prototype.getIndexedMeshArray=function(){return k(an(this.kB),VE)};WE.prototype.setScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);bn(c,a)};WE.prototype.__destroy__=function(){cn(this.kB)};function XE(){this.kB=dn();h(XE)[this.kB]=this}XE.prototype=Object.create(ZD.prototype);XE.prototype.constructor=XE;XE.prototype.lB=XE;XE.mB={};b.btEmptyShape=XE; +XE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);en(c,a)};XE.prototype.getLocalScaling=function(){return k(fn(this.kB),m)};XE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);gn(d,a,c)};XE.prototype.__destroy__=function(){hn(this.kB)};function YE(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=jn(a,c);h(YE)[this.kB]=this}YE.prototype=Object.create(ZD.prototype); +YE.prototype.constructor=YE;YE.prototype.lB=YE;YE.mB={};b.btStaticPlaneShape=YE;YE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);kn(c,a)};YE.prototype.getLocalScaling=function(){return k(ln(this.kB),m)};YE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);mn(d,a,c)};YE.prototype.__destroy__=function(){nn(this.kB)}; +function ZE(a,c,d){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);this.kB=void 0===d?on(a,c):pn(a,c,d);h(ZE)[this.kB]=this}ZE.prototype=Object.create(kE.prototype);ZE.prototype.constructor=ZE;ZE.prototype.lB=ZE;ZE.mB={};b.btBvhTriangleMeshShape=ZE;ZE.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);qn(c,a)};ZE.prototype.getLocalScaling=function(){return k(rn(this.kB),m)}; +ZE.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);sn(d,a,c)};ZE.prototype.__destroy__=function(){tn(this.kB)}; +function $E(a,c,d,e,g,n,z,T,Da){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);n&&"object"===typeof n&&(n=n.kB);z&&"object"===typeof z&&(z=z.kB);T&&"object"===typeof T&&(T=T.kB);Da&&"object"===typeof Da&&(Da=Da.kB);this.kB=un(a,c,d,e,g,n,z,T,Da);h($E)[this.kB]=this}$E.prototype=Object.create(ZD.prototype);$E.prototype.constructor=$E;$E.prototype.lB=$E;$E.mB={}; +b.btHeightfieldTerrainShape=$E;$E.prototype.setMargin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vn(c,a)};$E.prototype.getMargin=function(){return wn(this.kB)};$E.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xn(c,a)};$E.prototype.getLocalScaling=function(){return k(yn(this.kB),m)};$E.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);zn(d,a,c)}; +$E.prototype.__destroy__=function(){An(this.kB)};function aF(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=Bn(a,c,d,e);h(aF)[this.kB]=this}aF.prototype=Object.create(f.prototype);aF.prototype.constructor=aF;aF.prototype.lB=aF;aF.mB={};b.btAABB=aF;aF.prototype.invalidate=function(){Cn(this.kB)};aF.prototype.increment_margin=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dn(c,a)}; +aF.prototype.copy_with_margin=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);En(d,a,c)};aF.prototype.__destroy__=function(){Fn(this.kB)};function bF(){this.kB=Gn();h(bF)[this.kB]=this}bF.prototype=Object.create(f.prototype);bF.prototype.constructor=bF;bF.prototype.lB=bF;bF.mB={};b.btPrimitiveTriangle=bF;bF.prototype.__destroy__=function(){Hn(this.kB)}; +function cF(a,c,d){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);this.kB=In(a,c,d);h(cF)[this.kB]=this}cF.prototype=Object.create(f.prototype);cF.prototype.constructor=cF;cF.prototype.lB=cF;cF.mB={};b.btTriangleShapeEx=cF;cF.prototype.getAabb=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Jn(e,a,c,d)}; +cF.prototype.applyTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Kn(c,a)};cF.prototype.buildTriPlane=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ln(c,a)};cF.prototype.__destroy__=function(){Mn(this.kB)};function dF(){this.kB=Nn();h(dF)[this.kB]=this}dF.prototype=Object.create(f.prototype);dF.prototype.constructor=dF;dF.prototype.lB=dF;dF.mB={};b.btTetrahedronShapeEx=dF; +dF.prototype.setVertices=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);On(g,a,c,d,e)};dF.prototype.__destroy__=function(){Pn(this.kB)};function eF(){throw"cannot construct a CompoundPrimitiveManager, no constructor in IDL";}eF.prototype=Object.create(lE.prototype);eF.prototype.constructor=eF;eF.prototype.lB=eF;eF.mB={};b.CompoundPrimitiveManager=eF; +eF.prototype.get_primitive_count=function(){return Qn(this.kB)};eF.prototype.get_primitive_box=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Rn(d,a,c)};eF.prototype.get_primitive_triangle=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Sn(d,a,c)};eF.prototype.is_trimesh=function(){return!!Tn(this.kB)};eF.prototype.get_m_compoundShape=eF.prototype.VC=function(){return k(Un(this.kB),H)}; +eF.prototype.set_m_compoundShape=eF.prototype.MF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vn(c,a)};Object.defineProperty(eF.prototype,"m_compoundShape",{get:eF.prototype.VC,set:eF.prototype.MF});eF.prototype.__destroy__=function(){Wn(this.kB)};function H(a){a&&"object"===typeof a&&(a=a.kB);this.kB=void 0===a?Xn():Yn(a);h(H)[this.kB]=this}H.prototype=Object.create(w.prototype);H.prototype.constructor=H;H.prototype.lB=H;H.mB={};b.btGImpactCompoundShape=H; +H.prototype.childrenHasTransform=function(){return!!Zn(this.kB)};H.prototype.getPrimitiveManager=function(){return k($n(this.kB),lE)};H.prototype.getCompoundPrimitiveManager=function(){return k(ao(this.kB),eF)};H.prototype.getNumChildShapes=function(){return bo(this.kB)};H.prototype.addChildShape=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);co(d,a,c)}; +H.prototype.getChildShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(eo(c,a),l)};H.prototype.getChildAabb=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);fo(g,a,c,d,e)};H.prototype.getChildTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(go(c,a),r)}; +H.prototype.setChildTransform=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);ho(d,a,c)};H.prototype.calculateLocalInertia=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);io(d,a,c)};H.prototype.getName=function(){return sa(jo(this.kB))};H.prototype.getGImpactShapeType=function(){return ko(this.kB)};H.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);lo(c,a)}; +H.prototype.getLocalScaling=function(){return k(mo(this.kB),m)};H.prototype.updateBound=function(){no(this.kB)};H.prototype.postUpdate=function(){oo(this.kB)};H.prototype.getShapeType=function(){return po(this.kB)};H.prototype.needsRetrieveTriangles=function(){return!!qo(this.kB)};H.prototype.needsRetrieveTetrahedrons=function(){return!!ro(this.kB)};H.prototype.getBulletTriangle=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);so(d,a,c)}; +H.prototype.getBulletTetrahedron=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);to(d,a,c)};H.prototype.__destroy__=function(){uo(this.kB)};function I(a){a&&"object"===typeof a&&(a=a.kB);this.kB=void 0===a?vo():wo(a);h(I)[this.kB]=this}I.prototype=Object.create(lE.prototype);I.prototype.constructor=I;I.prototype.lB=I;I.mB={};b.TrimeshPrimitiveManager=I;I.prototype.lock=I.prototype.lock=function(){xo(this.kB)};I.prototype.unlock=I.prototype.unlock=function(){yo(this.kB)}; +I.prototype.is_trimesh=function(){return!!zo(this.kB)};I.prototype.get_vertex_count=function(){return Ao(this.kB)};I.prototype.get_indices=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);Bo(g,a,c,d,e)};I.prototype.get_vertex=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Co(d,a,c)}; +I.prototype.get_bullet_triangle=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Do(d,a,c)};I.prototype.get_m_margin=I.prototype.LD=function(){return Eo(this.kB)};I.prototype.set_m_margin=I.prototype.CG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Fo(c,a)};Object.defineProperty(I.prototype,"m_margin",{get:I.prototype.LD,set:I.prototype.CG});I.prototype.get_m_meshInterface=I.prototype.OD=function(){return k(Go(this.kB),jE)}; +I.prototype.set_m_meshInterface=I.prototype.FG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ho(c,a)};Object.defineProperty(I.prototype,"m_meshInterface",{get:I.prototype.OD,set:I.prototype.FG});I.prototype.get_m_part=I.prototype.VD=function(){return Io(this.kB)};I.prototype.set_m_part=I.prototype.MG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jo(c,a)};Object.defineProperty(I.prototype,"m_part",{get:I.prototype.VD,set:I.prototype.MG}); +I.prototype.get_m_lock_count=I.prototype.JD=function(){return Ko(this.kB)};I.prototype.set_m_lock_count=I.prototype.AG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Lo(c,a)};Object.defineProperty(I.prototype,"m_lock_count",{get:I.prototype.JD,set:I.prototype.AG});I.prototype.get_numverts=I.prototype.LE=function(){return Mo(this.kB)};I.prototype.set_numverts=I.prototype.DH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);No(c,a)}; +Object.defineProperty(I.prototype,"numverts",{get:I.prototype.LE,set:I.prototype.DH});I.prototype.get_type=I.prototype.PE=function(){return Oo(this.kB)};I.prototype.set_type=I.prototype.HH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Po(c,a)};Object.defineProperty(I.prototype,"type",{get:I.prototype.PE,set:I.prototype.HH});I.prototype.get_stride=I.prototype.NE=function(){return Qo(this.kB)}; +I.prototype.set_stride=I.prototype.FH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ro(c,a)};Object.defineProperty(I.prototype,"stride",{get:I.prototype.NE,set:I.prototype.FH});I.prototype.get_indexstride=I.prototype.gC=function(){return So(this.kB)};I.prototype.set_indexstride=I.prototype.YE=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);To(c,a)};Object.defineProperty(I.prototype,"indexstride",{get:I.prototype.gC,set:I.prototype.YE}); +I.prototype.get_numfaces=I.prototype.KE=function(){return Uo(this.kB)};I.prototype.set_numfaces=I.prototype.CH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vo(c,a)};Object.defineProperty(I.prototype,"numfaces",{get:I.prototype.KE,set:I.prototype.CH});I.prototype.get_indicestype=I.prototype.hC=function(){return Wo(this.kB)};I.prototype.set_indicestype=I.prototype.ZE=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Xo(c,a)}; +Object.defineProperty(I.prototype,"indicestype",{get:I.prototype.hC,set:I.prototype.ZE});I.prototype.__destroy__=function(){Yo(this.kB)};function fF(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=Zo(a,c);h(fF)[this.kB]=this}fF.prototype=Object.create(w.prototype);fF.prototype.constructor=fF;fF.prototype.lB=fF;fF.mB={};b.btGImpactMeshShapePart=fF;fF.prototype.getTrimeshPrimitiveManager=function(){return k($o(this.kB),I)};fF.prototype.getVertexCount=function(){return ap(this.kB)}; +fF.prototype.getVertex=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);bp(d,a,c)};fF.prototype.getPart=function(){return cp(this.kB)};fF.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);dp(c,a)};fF.prototype.getLocalScaling=function(){return k(ep(this.kB),m)};fF.prototype.updateBound=function(){fp(this.kB)};fF.prototype.postUpdate=function(){gp(this.kB)};fF.prototype.getShapeType=function(){return hp(this.kB)}; +fF.prototype.needsRetrieveTriangles=function(){return!!ip(this.kB)};fF.prototype.needsRetrieveTetrahedrons=function(){return!!jp(this.kB)};fF.prototype.getBulletTriangle=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);kp(d,a,c)};fF.prototype.getBulletTetrahedron=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);lp(d,a,c)};fF.prototype.__destroy__=function(){mp(this.kB)}; +function gF(a){a&&"object"===typeof a&&(a=a.kB);this.kB=np(a);h(gF)[this.kB]=this}gF.prototype=Object.create(w.prototype);gF.prototype.constructor=gF;gF.prototype.lB=gF;gF.mB={};b.btGImpactMeshShape=gF;gF.prototype.getMeshInterface=function(){return k(op(this.kB),jE)};gF.prototype.getMeshPartCount=function(){return pp(this.kB)};gF.prototype.getMeshPart=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(qp(c,a),fF)};gF.prototype.calculateSerializeBufferSize=function(){return rp(this.kB)}; +gF.prototype.setLocalScaling=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);sp(c,a)};gF.prototype.getLocalScaling=function(){return k(tp(this.kB),m)};gF.prototype.updateBound=function(){up(this.kB)};gF.prototype.postUpdate=function(){vp(this.kB)};gF.prototype.getShapeType=function(){return wp(this.kB)};gF.prototype.needsRetrieveTriangles=function(){return!!xp(this.kB)};gF.prototype.needsRetrieveTetrahedrons=function(){return!!yp(this.kB)}; +gF.prototype.getBulletTriangle=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);zp(d,a,c)};gF.prototype.getBulletTetrahedron=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Ap(d,a,c)};gF.prototype.__destroy__=function(){Bp(this.kB)}; +function hF(a,c){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);this.kB=void 0===a?Cp():void 0===c?_emscripten_bind_btCollisionAlgorithmConstructionInfo_btCollisionAlgorithmConstructionInfo_1(a):Dp(a,c);h(hF)[this.kB]=this}hF.prototype=Object.create(f.prototype);hF.prototype.constructor=hF;hF.prototype.lB=hF;hF.mB={};b.btCollisionAlgorithmConstructionInfo=hF;hF.prototype.get_m_dispatcher1=hF.prototype.eD=function(){return k(Ep(this.kB),TD)}; +hF.prototype.set_m_dispatcher1=hF.prototype.WF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Fp(c,a)};Object.defineProperty(hF.prototype,"m_dispatcher1",{get:hF.prototype.eD,set:hF.prototype.WF});hF.prototype.get_m_manifold=hF.prototype.KD=function(){return k(Gp(this.kB),oE)};hF.prototype.set_m_manifold=hF.prototype.BG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Hp(c,a)};Object.defineProperty(hF.prototype,"m_manifold",{get:hF.prototype.KD,set:hF.prototype.BG}); +hF.prototype.__destroy__=function(){Ip(this.kB)};function iF(a,c,d){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);this.kB=Jp(a,c,d);h(iF)[this.kB]=this}iF.prototype=Object.create(mE.prototype);iF.prototype.constructor=iF;iF.prototype.lB=iF;iF.mB={};b.btGImpactCollisionAlgorithm=iF;iF.prototype.registerAlgorithm=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Kp(c,a)};iF.prototype.__destroy__=function(){Lp(this.kB)}; +function jF(){this.kB=Mp();h(jF)[this.kB]=this}jF.prototype=Object.create(f.prototype);jF.prototype.constructor=jF;jF.prototype.lB=jF;jF.mB={};b.btDefaultCollisionConstructionInfo=jF;jF.prototype.__destroy__=function(){Np(this.kB)};function oE(){this.kB=Op();h(oE)[this.kB]=this}oE.prototype=Object.create(f.prototype);oE.prototype.constructor=oE;oE.prototype.lB=oE;oE.mB={};b.btPersistentManifold=oE;oE.prototype.getBody0=function(){return k(Pp(this.kB),q)}; +oE.prototype.getBody1=function(){return k(Qp(this.kB),q)};oE.prototype.getNumContacts=function(){return Rp(this.kB)};oE.prototype.getContactPoint=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Sp(c,a),E)};oE.prototype.__destroy__=function(){Tp(this.kB)};function kF(a){a&&"object"===typeof a&&(a=a.kB);this.kB=Up(a);h(kF)[this.kB]=this}kF.prototype=Object.create(TD.prototype);kF.prototype.constructor=kF;kF.prototype.lB=kF;kF.mB={};b.btCollisionDispatcher=kF; +kF.prototype.getNumManifolds=function(){return Vp(this.kB)};kF.prototype.getManifoldByIndexInternal=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Wp(c,a),oE)};kF.prototype.__destroy__=function(){Xp(this.kB)};function lF(){throw"cannot construct a btOverlappingPairCallback, no constructor in IDL";}lF.prototype=Object.create(f.prototype);lF.prototype.constructor=lF;lF.prototype.lB=lF;lF.mB={};b.btOverlappingPairCallback=lF;lF.prototype.__destroy__=function(){Yp(this.kB)}; +function UD(){throw"cannot construct a btOverlappingPairCache, no constructor in IDL";}UD.prototype=Object.create(f.prototype);UD.prototype.constructor=UD;UD.prototype.lB=UD;UD.mB={};b.btOverlappingPairCache=UD;UD.prototype.setInternalGhostPairCallback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Zp(c,a)};UD.prototype.getNumOverlappingPairs=function(){return $p(this.kB)};UD.prototype.__destroy__=function(){aq(this.kB)}; +function mF(a,c,d,e,g){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);this.kB=void 0===d?bq(a,c):void 0===e?cq(a,c,d):void 0===g?dq(a,c,d,e):eq(a,c,d,e,g);h(mF)[this.kB]=this}mF.prototype=Object.create(f.prototype);mF.prototype.constructor=mF;mF.prototype.lB=mF;mF.mB={};b.btAxisSweep3=mF;mF.prototype.__destroy__=function(){fq(this.kB)}; +function VD(){throw"cannot construct a btBroadphaseInterface, no constructor in IDL";}VD.prototype=Object.create(f.prototype);VD.prototype.constructor=VD;VD.prototype.lB=VD;VD.mB={};b.btBroadphaseInterface=VD;VD.prototype.getOverlappingPairCache=function(){return k(gq(this.kB),UD)};VD.prototype.__destroy__=function(){hq(this.kB)};function nF(){throw"cannot construct a btCollisionConfiguration, no constructor in IDL";}nF.prototype=Object.create(f.prototype);nF.prototype.constructor=nF; +nF.prototype.lB=nF;nF.mB={};b.btCollisionConfiguration=nF;nF.prototype.__destroy__=function(){iq(this.kB)};function oF(){this.kB=jq();h(oF)[this.kB]=this}oF.prototype=Object.create(f.prototype);oF.prototype.constructor=oF;oF.prototype.lB=oF;oF.mB={};b.btDbvtBroadphase=oF;oF.prototype.__destroy__=function(){kq(this.kB)};function YD(){throw"cannot construct a btBroadphaseProxy, no constructor in IDL";}YD.prototype=Object.create(f.prototype);YD.prototype.constructor=YD;YD.prototype.lB=YD;YD.mB={}; +b.btBroadphaseProxy=YD;YD.prototype.get_m_collisionFilterGroup=YD.prototype.nB=function(){return lq(this.kB)};YD.prototype.set_m_collisionFilterGroup=YD.prototype.pB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);mq(c,a)};Object.defineProperty(YD.prototype,"m_collisionFilterGroup",{get:YD.prototype.nB,set:YD.prototype.pB});YD.prototype.get_m_collisionFilterMask=YD.prototype.oB=function(){return nq(this.kB)}; +YD.prototype.set_m_collisionFilterMask=YD.prototype.qB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);oq(c,a)};Object.defineProperty(YD.prototype,"m_collisionFilterMask",{get:YD.prototype.oB,set:YD.prototype.qB});YD.prototype.__destroy__=function(){pq(this.kB)};function J(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=void 0===e?qq(a,c,d):rq(a,c,d,e);h(J)[this.kB]=this}J.prototype=Object.create(f.prototype); +J.prototype.constructor=J;J.prototype.lB=J;J.mB={};b.btRigidBodyConstructionInfo=J;J.prototype.get_m_linearDamping=J.prototype.DD=function(){return sq(this.kB)};J.prototype.set_m_linearDamping=J.prototype.uG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);tq(c,a)};Object.defineProperty(J.prototype,"m_linearDamping",{get:J.prototype.DD,set:J.prototype.uG});J.prototype.get_m_angularDamping=J.prototype.HC=function(){return uq(this.kB)}; +J.prototype.set_m_angularDamping=J.prototype.yF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vq(c,a)};Object.defineProperty(J.prototype,"m_angularDamping",{get:J.prototype.HC,set:J.prototype.yF});J.prototype.get_m_friction=J.prototype.kD=function(){return wq(this.kB)};J.prototype.set_m_friction=J.prototype.bG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xq(c,a)};Object.defineProperty(J.prototype,"m_friction",{get:J.prototype.kD,set:J.prototype.bG}); +J.prototype.get_m_rollingFriction=J.prototype.dE=function(){return yq(this.kB)};J.prototype.set_m_rollingFriction=J.prototype.WG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zq(c,a)};Object.defineProperty(J.prototype,"m_rollingFriction",{get:J.prototype.dE,set:J.prototype.WG});J.prototype.get_m_restitution=J.prototype.bE=function(){return Aq(this.kB)};J.prototype.set_m_restitution=J.prototype.UG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Bq(c,a)}; +Object.defineProperty(J.prototype,"m_restitution",{get:J.prototype.bE,set:J.prototype.UG});J.prototype.get_m_linearSleepingThreshold=J.prototype.ED=function(){return Cq(this.kB)};J.prototype.set_m_linearSleepingThreshold=J.prototype.vG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dq(c,a)};Object.defineProperty(J.prototype,"m_linearSleepingThreshold",{get:J.prototype.ED,set:J.prototype.vG});J.prototype.get_m_angularSleepingThreshold=J.prototype.IC=function(){return Eq(this.kB)}; +J.prototype.set_m_angularSleepingThreshold=J.prototype.zF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Fq(c,a)};Object.defineProperty(J.prototype,"m_angularSleepingThreshold",{get:J.prototype.IC,set:J.prototype.zF});J.prototype.get_m_additionalDamping=J.prototype.CC=function(){return!!Gq(this.kB)};J.prototype.set_m_additionalDamping=J.prototype.tF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Hq(c,a)}; +Object.defineProperty(J.prototype,"m_additionalDamping",{get:J.prototype.CC,set:J.prototype.tF});J.prototype.get_m_additionalDampingFactor=J.prototype.DC=function(){return Iq(this.kB)};J.prototype.set_m_additionalDampingFactor=J.prototype.uF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jq(c,a)};Object.defineProperty(J.prototype,"m_additionalDampingFactor",{get:J.prototype.DC,set:J.prototype.uF});J.prototype.get_m_additionalLinearDampingThresholdSqr=J.prototype.EC=function(){return Kq(this.kB)}; +J.prototype.set_m_additionalLinearDampingThresholdSqr=J.prototype.vF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Lq(c,a)};Object.defineProperty(J.prototype,"m_additionalLinearDampingThresholdSqr",{get:J.prototype.EC,set:J.prototype.vF});J.prototype.get_m_additionalAngularDampingThresholdSqr=J.prototype.BC=function(){return Mq(this.kB)};J.prototype.set_m_additionalAngularDampingThresholdSqr=J.prototype.sF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Nq(c,a)}; +Object.defineProperty(J.prototype,"m_additionalAngularDampingThresholdSqr",{get:J.prototype.BC,set:J.prototype.sF});J.prototype.get_m_additionalAngularDampingFactor=J.prototype.AC=function(){return Oq(this.kB)};J.prototype.set_m_additionalAngularDampingFactor=J.prototype.rF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Pq(c,a)};Object.defineProperty(J.prototype,"m_additionalAngularDampingFactor",{get:J.prototype.AC,set:J.prototype.rF});J.prototype.__destroy__=function(){Qq(this.kB)}; +function K(a){a&&"object"===typeof a&&(a=a.kB);this.kB=Rq(a);h(K)[this.kB]=this}K.prototype=Object.create(q.prototype);K.prototype.constructor=K;K.prototype.lB=K;K.mB={};b.btRigidBody=K;K.prototype.getCenterOfMassTransform=function(){return k(Sq(this.kB),r)};K.prototype.setCenterOfMassTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Tq(c,a)};K.prototype.setSleepingThresholds=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Uq(d,a,c)}; +K.prototype.getLinearDamping=function(){return Vq(this.kB)};K.prototype.getAngularDamping=function(){return Wq(this.kB)};K.prototype.setDamping=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Xq(d,a,c)};K.prototype.setMassProps=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Yq(d,a,c)};K.prototype.getLinearFactor=function(){return k(Zq(this.kB),m)}; +K.prototype.setLinearFactor=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$q(c,a)};K.prototype.applyTorque=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ar(c,a)};K.prototype.applyLocalTorque=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);br(c,a)};K.prototype.applyForce=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);cr(d,a,c)}; +K.prototype.applyCentralForce=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);dr(c,a)};K.prototype.applyCentralLocalForce=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);er(c,a)};K.prototype.applyTorqueImpulse=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fr(c,a)};K.prototype.applyImpulse=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);gr(d,a,c)}; +K.prototype.applyCentralImpulse=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hr(c,a)};K.prototype.updateInertiaTensor=function(){ir(this.kB)};K.prototype.getLinearVelocity=function(){return k(jr(this.kB),m)};K.prototype.getAngularVelocity=function(){return k(kr(this.kB),m)};K.prototype.setLinearVelocity=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);lr(c,a)};K.prototype.setAngularVelocity=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);mr(c,a)}; +K.prototype.getMotionState=function(){return k(nr(this.kB),dE)};K.prototype.setMotionState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);or(c,a)};K.prototype.getAngularFactor=function(){return k(pr(this.kB),m)};K.prototype.setAngularFactor=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);qr(c,a)};K.prototype.upcast=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(rr(c,a),K)}; +K.prototype.getAabb=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);sr(d,a,c)};K.prototype.applyGravity=function(){tr(this.kB)};K.prototype.getGravity=function(){return k(ur(this.kB),m)};K.prototype.setGravity=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vr(c,a)};K.prototype.getBroadphaseProxy=function(){return k(wr(this.kB),YD)};K.prototype.clearForces=function(){xr(this.kB)}; +K.prototype.setFlags=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yr(c,a)};K.prototype.getFlags=function(){return zr(this.kB)};K.prototype.setAnisotropicFriction=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Ar(d,a,c)};K.prototype.getCollisionShape=function(){return k(Br(this.kB),l)};K.prototype.setContactProcessingThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Cr(c,a)}; +K.prototype.setActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dr(c,a)};K.prototype.forceActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Er(c,a)};K.prototype.activate=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);void 0===a?Fr(c):Gr(c,a)};K.prototype.isActive=function(){return!!Hr(this.kB)};K.prototype.isKinematicObject=function(){return!!Ir(this.kB)};K.prototype.isStaticObject=function(){return!!Jr(this.kB)}; +K.prototype.isStaticOrKinematicObject=function(){return!!Kr(this.kB)};K.prototype.getRestitution=function(){return Lr(this.kB)};K.prototype.getFriction=function(){return Mr(this.kB)};K.prototype.getRollingFriction=function(){return Nr(this.kB)};K.prototype.setRestitution=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Or(c,a)};K.prototype.setFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Pr(c,a)}; +K.prototype.setRollingFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Qr(c,a)};K.prototype.getWorldTransform=function(){return k(Rr(this.kB),r)};K.prototype.getCollisionFlags=function(){return Sr(this.kB)};K.prototype.setCollisionFlags=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Tr(c,a)};K.prototype.setWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ur(c,a)}; +K.prototype.setCollisionShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vr(c,a)};K.prototype.setCcdMotionThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Wr(c,a)};K.prototype.setCcdSweptSphereRadius=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Xr(c,a)};K.prototype.getUserIndex=function(){return Yr(this.kB)};K.prototype.setUserIndex=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Zr(c,a)}; +K.prototype.getUserPointer=function(){return k($r(this.kB),XD)};K.prototype.setUserPointer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);as(c,a)};K.prototype.getBroadphaseHandle=function(){return k(bs(this.kB),YD)};K.prototype.__destroy__=function(){cs(this.kB)};function L(){this.kB=ds();h(L)[this.kB]=this}L.prototype=Object.create(f.prototype);L.prototype.constructor=L;L.prototype.lB=L;L.mB={};b.btConstraintSetting=L;L.prototype.get_m_tau=L.prototype.sE=function(){return es(this.kB)}; +L.prototype.set_m_tau=L.prototype.kH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);gs(c,a)};Object.defineProperty(L.prototype,"m_tau",{get:L.prototype.sE,set:L.prototype.kH});L.prototype.get_m_damping=L.prototype.aD=function(){return hs(this.kB)};L.prototype.set_m_damping=L.prototype.SF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);is(c,a)};Object.defineProperty(L.prototype,"m_damping",{get:L.prototype.aD,set:L.prototype.SF}); +L.prototype.get_m_impulseClamp=L.prototype.wD=function(){return js(this.kB)};L.prototype.set_m_impulseClamp=L.prototype.nG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ks(c,a)};Object.defineProperty(L.prototype,"m_impulseClamp",{get:L.prototype.wD,set:L.prototype.nG});L.prototype.__destroy__=function(){ls(this.kB)}; +function pF(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=void 0===d?ms(a,c):void 0===e?_emscripten_bind_btPoint2PointConstraint_btPoint2PointConstraint_3(a,c,d):ns(a,c,d,e);h(pF)[this.kB]=this}pF.prototype=Object.create(aE.prototype);pF.prototype.constructor=pF;pF.prototype.lB=pF;pF.mB={};b.btPoint2PointConstraint=pF; +pF.prototype.setPivotA=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ps(c,a)};pF.prototype.setPivotB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);qs(c,a)};pF.prototype.getPivotInA=function(){return k(rs(this.kB),m)};pF.prototype.getPivotInB=function(){return k(ss(this.kB),m)};pF.prototype.enableFeedback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ts(c,a)};pF.prototype.getBreakingImpulseThreshold=function(){return us(this.kB)}; +pF.prototype.setBreakingImpulseThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vs(c,a)};pF.prototype.getParam=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return xs(d,a,c)};pF.prototype.setParam=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);ys(e,a,c,d)};pF.prototype.get_m_setting=pF.prototype.fE=function(){return k(zs(this.kB),L)}; +pF.prototype.set_m_setting=pF.prototype.YG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);As(c,a)};Object.defineProperty(pF.prototype,"m_setting",{get:pF.prototype.fE,set:pF.prototype.YG});pF.prototype.__destroy__=function(){Bs(this.kB)}; +function qF(a,c,d,e,g){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);this.kB=void 0===e?Cs(a,c,d):void 0===g?_emscripten_bind_btGeneric6DofSpringConstraint_btGeneric6DofSpringConstraint_4(a,c,d,e):Ds(a,c,d,e,g);h(qF)[this.kB]=this}qF.prototype=Object.create(pE.prototype);qF.prototype.constructor=qF;qF.prototype.lB=qF;qF.mB={};b.btGeneric6DofSpringConstraint=qF; +qF.prototype.enableSpring=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Es(d,a,c)};qF.prototype.setStiffness=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Fs(d,a,c)};qF.prototype.setDamping=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Gs(d,a,c)}; +qF.prototype.setEquilibriumPoint=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);void 0===a?Hs(d):void 0===c?Is(d,a):Js(d,a,c)};qF.prototype.setLinearLowerLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ks(c,a)};qF.prototype.setLinearUpperLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ls(c,a)};qF.prototype.setAngularLowerLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ms(c,a)}; +qF.prototype.setAngularUpperLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ns(c,a)};qF.prototype.getFrameOffsetA=function(){return k(Os(this.kB),r)};qF.prototype.enableFeedback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ps(c,a)};qF.prototype.getBreakingImpulseThreshold=function(){return Qs(this.kB)};qF.prototype.setBreakingImpulseThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Rs(c,a)}; +qF.prototype.getParam=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return Ss(d,a,c)};qF.prototype.setParam=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Ts(e,a,c,d)};qF.prototype.__destroy__=function(){Us(this.kB)};function rF(){this.kB=Vs();h(rF)[this.kB]=this}rF.prototype=Object.create(f.prototype);rF.prototype.constructor=rF;rF.prototype.lB=rF;rF.mB={}; +b.btSequentialImpulseConstraintSolver=rF;rF.prototype.__destroy__=function(){Ws(this.kB)};function sF(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=void 0===d?Xs(a,c):void 0===e?_emscripten_bind_btConeTwistConstraint_btConeTwistConstraint_3(a,c,d):Ys(a,c,d,e);h(sF)[this.kB]=this}sF.prototype=Object.create(aE.prototype);sF.prototype.constructor=sF;sF.prototype.lB=sF;sF.mB={}; +b.btConeTwistConstraint=sF;sF.prototype.setLimit=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Zs(d,a,c)};sF.prototype.setAngularOnly=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$s(c,a)};sF.prototype.setDamping=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);at(c,a)};sF.prototype.enableMotor=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);bt(c,a)}; +sF.prototype.setMaxMotorImpulse=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ct(c,a)};sF.prototype.setMaxMotorImpulseNormalized=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);dt(c,a)};sF.prototype.setMotorTarget=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);et(c,a)};sF.prototype.setMotorTargetInConstraintSpace=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ft(c,a)}; +sF.prototype.enableFeedback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);gt(c,a)};sF.prototype.getBreakingImpulseThreshold=function(){return ht(this.kB)};sF.prototype.setBreakingImpulseThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);it(c,a)};sF.prototype.getParam=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return jt(d,a,c)}; +sF.prototype.setParam=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);kt(e,a,c,d)};sF.prototype.__destroy__=function(){lt(this.kB)}; +function tF(a,c,d,e,g,n,z){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);n&&"object"===typeof n&&(n=n.kB);z&&"object"===typeof z&&(z=z.kB);this.kB=void 0===d?mt(a,c):void 0===e?nt(a,c,d):void 0===g?ot(a,c,d,e):void 0===n?pt(a,c,d,e,g):void 0===z?qt(a,c,d,e,g,n):rt(a,c,d,e,g,n,z);h(tF)[this.kB]=this}tF.prototype=Object.create(aE.prototype);tF.prototype.constructor=tF; +tF.prototype.lB=tF;tF.mB={};b.btHingeConstraint=tF;tF.prototype.getHingeAngle=function(){return st(this.kB)};tF.prototype.setLimit=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);void 0===g?tt(n,a,c,d,e):ut(n,a,c,d,e,g)}; +tF.prototype.enableAngularMotor=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);vt(e,a,c,d)};tF.prototype.setAngularOnly=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);wt(c,a)};tF.prototype.enableMotor=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xt(c,a)};tF.prototype.setMaxMotorImpulse=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yt(c,a)}; +tF.prototype.setMotorTarget=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);zt(d,a,c)};tF.prototype.enableFeedback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);At(c,a)};tF.prototype.getBreakingImpulseThreshold=function(){return Bt(this.kB)};tF.prototype.setBreakingImpulseThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ct(c,a)}; +tF.prototype.getParam=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return Dt(d,a,c)};tF.prototype.setParam=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Et(e,a,c,d)};tF.prototype.__destroy__=function(){Ft(this.kB)}; +function uF(a,c,d,e,g){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);this.kB=void 0===e?Gt(a,c,d):void 0===g?_emscripten_bind_btSliderConstraint_btSliderConstraint_4(a,c,d,e):Ht(a,c,d,e,g);h(uF)[this.kB]=this}uF.prototype=Object.create(aE.prototype);uF.prototype.constructor=uF;uF.prototype.lB=uF;uF.mB={};b.btSliderConstraint=uF;uF.prototype.getLinearPos=function(){return It(this.kB)}; +uF.prototype.getAngularPos=function(){return Jt(this.kB)};uF.prototype.setLowerLinLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Kt(c,a)};uF.prototype.setUpperLinLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Lt(c,a)};uF.prototype.setLowerAngLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Mt(c,a)};uF.prototype.setUpperAngLimit=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Nt(c,a)}; +uF.prototype.setPoweredLinMotor=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ot(c,a)};uF.prototype.setMaxLinMotorForce=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Pt(c,a)};uF.prototype.setTargetLinMotorVelocity=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Qt(c,a)};uF.prototype.enableFeedback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Rt(c,a)};uF.prototype.getBreakingImpulseThreshold=function(){return St(this.kB)}; +uF.prototype.setBreakingImpulseThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Tt(c,a)};uF.prototype.getParam=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return Ut(d,a,c)};uF.prototype.setParam=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);Vt(e,a,c,d)};uF.prototype.__destroy__=function(){Wt(this.kB)}; +function vF(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=Xt(a,c,d,e);h(vF)[this.kB]=this}vF.prototype=Object.create(aE.prototype);vF.prototype.constructor=vF;vF.prototype.lB=vF;vF.mB={};b.btFixedConstraint=vF;vF.prototype.enableFeedback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Yt(c,a)};vF.prototype.getBreakingImpulseThreshold=function(){return Zt(this.kB)}; +vF.prototype.setBreakingImpulseThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$t(c,a)};vF.prototype.getParam=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return au(d,a,c)};vF.prototype.setParam=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);bu(e,a,c,d)};vF.prototype.__destroy__=function(){cu(this.kB)}; +function wF(){throw"cannot construct a btConstraintSolver, no constructor in IDL";}wF.prototype=Object.create(f.prototype);wF.prototype.constructor=wF;wF.prototype.lB=wF;wF.mB={};b.btConstraintSolver=wF;wF.prototype.__destroy__=function(){du(this.kB)};function p(){throw"cannot construct a btDispatcherInfo, no constructor in IDL";}p.prototype=Object.create(f.prototype);p.prototype.constructor=p;p.prototype.lB=p;p.mB={};b.btDispatcherInfo=p;p.prototype.get_m_timeStep=p.prototype.uE=function(){return eu(this.kB)}; +p.prototype.set_m_timeStep=p.prototype.mH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fu(c,a)};Object.defineProperty(p.prototype,"m_timeStep",{get:p.prototype.uE,set:p.prototype.mH});p.prototype.get_m_stepCount=p.prototype.lE=function(){return gu(this.kB)};p.prototype.set_m_stepCount=p.prototype.dH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hu(c,a)};Object.defineProperty(p.prototype,"m_stepCount",{get:p.prototype.lE,set:p.prototype.dH}); +p.prototype.get_m_dispatchFunc=p.prototype.cD=function(){return iu(this.kB)};p.prototype.set_m_dispatchFunc=p.prototype.UF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ju(c,a)};Object.defineProperty(p.prototype,"m_dispatchFunc",{get:p.prototype.cD,set:p.prototype.UF});p.prototype.get_m_timeOfImpact=p.prototype.tE=function(){return ku(this.kB)};p.prototype.set_m_timeOfImpact=p.prototype.lH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);lu(c,a)}; +Object.defineProperty(p.prototype,"m_timeOfImpact",{get:p.prototype.tE,set:p.prototype.lH});p.prototype.get_m_useContinuous=p.prototype.wE=function(){return!!mu(this.kB)};p.prototype.set_m_useContinuous=p.prototype.oH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);nu(c,a)};Object.defineProperty(p.prototype,"m_useContinuous",{get:p.prototype.wE,set:p.prototype.oH});p.prototype.get_m_enableSatConvex=p.prototype.hD=function(){return!!ou(this.kB)}; +p.prototype.set_m_enableSatConvex=p.prototype.ZF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pu(c,a)};Object.defineProperty(p.prototype,"m_enableSatConvex",{get:p.prototype.hD,set:p.prototype.ZF});p.prototype.get_m_enableSPU=p.prototype.gD=function(){return!!qu(this.kB)};p.prototype.set_m_enableSPU=p.prototype.YF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ru(c,a)};Object.defineProperty(p.prototype,"m_enableSPU",{get:p.prototype.gD,set:p.prototype.YF}); +p.prototype.get_m_useEpa=p.prototype.yE=function(){return!!su(this.kB)};p.prototype.set_m_useEpa=p.prototype.qH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);tu(c,a)};Object.defineProperty(p.prototype,"m_useEpa",{get:p.prototype.yE,set:p.prototype.qH});p.prototype.get_m_allowedCcdPenetration=p.prototype.FC=function(){return uu(this.kB)};p.prototype.set_m_allowedCcdPenetration=p.prototype.wF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vu(c,a)}; +Object.defineProperty(p.prototype,"m_allowedCcdPenetration",{get:p.prototype.FC,set:p.prototype.wF});p.prototype.get_m_useConvexConservativeDistanceUtil=p.prototype.xE=function(){return!!wu(this.kB)};p.prototype.set_m_useConvexConservativeDistanceUtil=p.prototype.pH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xu(c,a)};Object.defineProperty(p.prototype,"m_useConvexConservativeDistanceUtil",{get:p.prototype.xE,set:p.prototype.pH}); +p.prototype.get_m_convexConservativeDistanceThreshold=p.prototype.YC=function(){return yu(this.kB)};p.prototype.set_m_convexConservativeDistanceThreshold=p.prototype.PF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zu(c,a)};Object.defineProperty(p.prototype,"m_convexConservativeDistanceThreshold",{get:p.prototype.YC,set:p.prototype.PF});p.prototype.__destroy__=function(){Au(this.kB)};function t(){throw"cannot construct a btContactSolverInfo, no constructor in IDL";}t.prototype=Object.create(f.prototype); +t.prototype.constructor=t;t.prototype.lB=t;t.mB={};b.btContactSolverInfo=t;t.prototype.get_m_splitImpulse=t.prototype.iE=function(){return!!Bu(this.kB)};t.prototype.set_m_splitImpulse=t.prototype.aH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Cu(c,a)};Object.defineProperty(t.prototype,"m_splitImpulse",{get:t.prototype.iE,set:t.prototype.aH});t.prototype.get_m_splitImpulsePenetrationThreshold=t.prototype.jE=function(){return Du(this.kB)}; +t.prototype.set_m_splitImpulsePenetrationThreshold=t.prototype.bH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Eu(c,a)};Object.defineProperty(t.prototype,"m_splitImpulsePenetrationThreshold",{get:t.prototype.jE,set:t.prototype.bH});t.prototype.get_m_numIterations=t.prototype.TD=function(){return Fu(this.kB)};t.prototype.set_m_numIterations=t.prototype.KG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Gu(c,a)}; +Object.defineProperty(t.prototype,"m_numIterations",{get:t.prototype.TD,set:t.prototype.KG});t.prototype.__destroy__=function(){Hu(this.kB)};function M(){this.kB=Iu();h(M)[this.kB]=this}M.prototype=Object.create(f.prototype);M.prototype.constructor=M;M.prototype.lB=M;M.mB={};b.btVehicleTuning=M;M.prototype.get_m_suspensionStiffness=M.prototype.BB=function(){return Ju(this.kB)};M.prototype.set_m_suspensionStiffness=M.prototype.IB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ku(c,a)}; +Object.defineProperty(M.prototype,"m_suspensionStiffness",{get:M.prototype.BB,set:M.prototype.IB});M.prototype.get_m_suspensionCompression=M.prototype.mE=function(){return Lu(this.kB)};M.prototype.set_m_suspensionCompression=M.prototype.eH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Mu(c,a)};Object.defineProperty(M.prototype,"m_suspensionCompression",{get:M.prototype.mE,set:M.prototype.eH});M.prototype.get_m_suspensionDamping=M.prototype.nE=function(){return Nu(this.kB)}; +M.prototype.set_m_suspensionDamping=M.prototype.fH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ou(c,a)};Object.defineProperty(M.prototype,"m_suspensionDamping",{get:M.prototype.nE,set:M.prototype.fH});M.prototype.get_m_maxSuspensionTravelCm=M.prototype.AB=function(){return Pu(this.kB)};M.prototype.set_m_maxSuspensionTravelCm=M.prototype.HB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Qu(c,a)}; +Object.defineProperty(M.prototype,"m_maxSuspensionTravelCm",{get:M.prototype.AB,set:M.prototype.HB});M.prototype.get_m_frictionSlip=M.prototype.wB=function(){return Ru(this.kB)};M.prototype.set_m_frictionSlip=M.prototype.DB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Su(c,a)};Object.defineProperty(M.prototype,"m_frictionSlip",{get:M.prototype.wB,set:M.prototype.DB});M.prototype.get_m_maxSuspensionForce=M.prototype.zB=function(){return Tu(this.kB)}; +M.prototype.set_m_maxSuspensionForce=M.prototype.GB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Uu(c,a)};Object.defineProperty(M.prototype,"m_maxSuspensionForce",{get:M.prototype.zB,set:M.prototype.GB});function xF(){throw"cannot construct a btVehicleRaycasterResult, no constructor in IDL";}xF.prototype=Object.create(f.prototype);xF.prototype.constructor=xF;xF.prototype.lB=xF;xF.mB={};b.btVehicleRaycasterResult=xF; +xF.prototype.get_m_hitPointInWorld=xF.prototype.tD=function(){return k(Vu(this.kB),m)};xF.prototype.set_m_hitPointInWorld=xF.prototype.kG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Wu(c,a)};Object.defineProperty(xF.prototype,"m_hitPointInWorld",{get:xF.prototype.tD,set:xF.prototype.kG});xF.prototype.get_m_hitNormalInWorld=xF.prototype.rD=function(){return k(Xu(this.kB),m)}; +xF.prototype.set_m_hitNormalInWorld=xF.prototype.iG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Yu(c,a)};Object.defineProperty(xF.prototype,"m_hitNormalInWorld",{get:xF.prototype.rD,set:xF.prototype.iG});xF.prototype.get_m_distFraction=xF.prototype.fD=function(){return Zu(this.kB)};xF.prototype.set_m_distFraction=xF.prototype.XF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$u(c,a)};Object.defineProperty(xF.prototype,"m_distFraction",{get:xF.prototype.fD,set:xF.prototype.XF}); +xF.prototype.__destroy__=function(){av(this.kB)};function yF(a){a&&"object"===typeof a&&(a=a.kB);this.kB=bv(a);h(yF)[this.kB]=this}yF.prototype=Object.create(qE.prototype);yF.prototype.constructor=yF;yF.prototype.lB=yF;yF.mB={};b.btDefaultVehicleRaycaster=yF;yF.prototype.castRay=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);cv(e,a,c,d)};yF.prototype.__destroy__=function(){dv(this.kB)}; +function N(){throw"cannot construct a RaycastInfo, no constructor in IDL";}N.prototype=Object.create(f.prototype);N.prototype.constructor=N;N.prototype.lB=N;N.mB={};b.RaycastInfo=N;N.prototype.get_m_contactNormalWS=N.prototype.WC=function(){return k(ev(this.kB),m)};N.prototype.set_m_contactNormalWS=N.prototype.NF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fv(c,a)};Object.defineProperty(N.prototype,"m_contactNormalWS",{get:N.prototype.WC,set:N.prototype.NF}); +N.prototype.get_m_contactPointWS=N.prototype.XC=function(){return k(gv(this.kB),m)};N.prototype.set_m_contactPointWS=N.prototype.OF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hv(c,a)};Object.defineProperty(N.prototype,"m_contactPointWS",{get:N.prototype.XC,set:N.prototype.OF});N.prototype.get_m_suspensionLength=N.prototype.oE=function(){return iv(this.kB)};N.prototype.set_m_suspensionLength=N.prototype.gH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jv(c,a)}; +Object.defineProperty(N.prototype,"m_suspensionLength",{get:N.prototype.oE,set:N.prototype.gH});N.prototype.get_m_hardPointWS=N.prototype.oD=function(){return k(kv(this.kB),m)};N.prototype.set_m_hardPointWS=N.prototype.fG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);lv(c,a)};Object.defineProperty(N.prototype,"m_hardPointWS",{get:N.prototype.oD,set:N.prototype.fG});N.prototype.get_m_wheelDirectionWS=N.prototype.DE=function(){return k(mv(this.kB),m)}; +N.prototype.set_m_wheelDirectionWS=N.prototype.vH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);nv(c,a)};Object.defineProperty(N.prototype,"m_wheelDirectionWS",{get:N.prototype.DE,set:N.prototype.vH});N.prototype.get_m_wheelAxleWS=N.prototype.CE=function(){return k(ov(this.kB),m)};N.prototype.set_m_wheelAxleWS=N.prototype.uH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pv(c,a)};Object.defineProperty(N.prototype,"m_wheelAxleWS",{get:N.prototype.CE,set:N.prototype.uH}); +N.prototype.get_m_isInContact=N.prototype.zD=function(){return!!qv(this.kB)};N.prototype.set_m_isInContact=N.prototype.qG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);rv(c,a)};Object.defineProperty(N.prototype,"m_isInContact",{get:N.prototype.zD,set:N.prototype.qG});N.prototype.get_m_groundObject=N.prototype.nD=function(){return sv(this.kB)};N.prototype.set_m_groundObject=N.prototype.eG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);tv(c,a)}; +Object.defineProperty(N.prototype,"m_groundObject",{get:N.prototype.nD,set:N.prototype.eG});N.prototype.__destroy__=function(){uv(this.kB)};function O(){throw"cannot construct a btWheelInfoConstructionInfo, no constructor in IDL";}O.prototype=Object.create(f.prototype);O.prototype.constructor=O;O.prototype.lB=O;O.mB={};b.btWheelInfoConstructionInfo=O;O.prototype.get_m_chassisConnectionCS=O.prototype.RC=function(){return k(vv(this.kB),m)}; +O.prototype.set_m_chassisConnectionCS=O.prototype.IF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);wv(c,a)};Object.defineProperty(O.prototype,"m_chassisConnectionCS",{get:O.prototype.RC,set:O.prototype.IF});O.prototype.get_m_wheelDirectionCS=O.prototype.QB=function(){return k(xv(this.kB),m)};O.prototype.set_m_wheelDirectionCS=O.prototype.$B=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yv(c,a)};Object.defineProperty(O.prototype,"m_wheelDirectionCS",{get:O.prototype.QB,set:O.prototype.$B}); +O.prototype.get_m_wheelAxleCS=O.prototype.PB=function(){return k(zv(this.kB),m)};O.prototype.set_m_wheelAxleCS=O.prototype.ZB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Av(c,a)};Object.defineProperty(O.prototype,"m_wheelAxleCS",{get:O.prototype.PB,set:O.prototype.ZB});O.prototype.get_m_suspensionRestLength=O.prototype.qE=function(){return Bv(this.kB)};O.prototype.set_m_suspensionRestLength=O.prototype.iH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Cv(c,a)}; +Object.defineProperty(O.prototype,"m_suspensionRestLength",{get:O.prototype.qE,set:O.prototype.iH});O.prototype.get_m_maxSuspensionTravelCm=O.prototype.AB=function(){return Dv(this.kB)};O.prototype.set_m_maxSuspensionTravelCm=O.prototype.HB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ev(c,a)};Object.defineProperty(O.prototype,"m_maxSuspensionTravelCm",{get:O.prototype.AB,set:O.prototype.HB});O.prototype.get_m_wheelRadius=O.prototype.EE=function(){return Fv(this.kB)}; +O.prototype.set_m_wheelRadius=O.prototype.wH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Gv(c,a)};Object.defineProperty(O.prototype,"m_wheelRadius",{get:O.prototype.EE,set:O.prototype.wH});O.prototype.get_m_suspensionStiffness=O.prototype.BB=function(){return Hv(this.kB)};O.prototype.set_m_suspensionStiffness=O.prototype.IB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Iv(c,a)};Object.defineProperty(O.prototype,"m_suspensionStiffness",{get:O.prototype.BB,set:O.prototype.IB}); +O.prototype.get_m_wheelsDampingCompression=O.prototype.RB=function(){return Jv(this.kB)};O.prototype.set_m_wheelsDampingCompression=O.prototype.aC=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Kv(c,a)};Object.defineProperty(O.prototype,"m_wheelsDampingCompression",{get:O.prototype.RB,set:O.prototype.aC});O.prototype.get_m_wheelsDampingRelaxation=O.prototype.SB=function(){return Lv(this.kB)}; +O.prototype.set_m_wheelsDampingRelaxation=O.prototype.bC=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Mv(c,a)};Object.defineProperty(O.prototype,"m_wheelsDampingRelaxation",{get:O.prototype.SB,set:O.prototype.bC});O.prototype.get_m_frictionSlip=O.prototype.wB=function(){return Nv(this.kB)};O.prototype.set_m_frictionSlip=O.prototype.DB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ov(c,a)};Object.defineProperty(O.prototype,"m_frictionSlip",{get:O.prototype.wB,set:O.prototype.DB}); +O.prototype.get_m_maxSuspensionForce=O.prototype.zB=function(){return Pv(this.kB)};O.prototype.set_m_maxSuspensionForce=O.prototype.GB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Qv(c,a)};Object.defineProperty(O.prototype,"m_maxSuspensionForce",{get:O.prototype.zB,set:O.prototype.GB});O.prototype.get_m_bIsFrontWheel=O.prototype.JB=function(){return!!Rv(this.kB)};O.prototype.set_m_bIsFrontWheel=O.prototype.TB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Sv(c,a)}; +Object.defineProperty(O.prototype,"m_bIsFrontWheel",{get:O.prototype.JB,set:O.prototype.TB});O.prototype.__destroy__=function(){Tv(this.kB)};function P(a){a&&"object"===typeof a&&(a=a.kB);this.kB=Uv(a);h(P)[this.kB]=this}P.prototype=Object.create(f.prototype);P.prototype.constructor=P;P.prototype.lB=P;P.mB={};b.btWheelInfo=P;P.prototype.getSuspensionRestLength=function(){return Vv(this.kB)}; +P.prototype.updateWheel=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Wv(d,a,c)};P.prototype.get_m_suspensionStiffness=P.prototype.BB=function(){return Xv(this.kB)};P.prototype.set_m_suspensionStiffness=P.prototype.IB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Yv(c,a)};Object.defineProperty(P.prototype,"m_suspensionStiffness",{get:P.prototype.BB,set:P.prototype.IB});P.prototype.get_m_frictionSlip=P.prototype.wB=function(){return Zv(this.kB)}; +P.prototype.set_m_frictionSlip=P.prototype.DB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$v(c,a)};Object.defineProperty(P.prototype,"m_frictionSlip",{get:P.prototype.wB,set:P.prototype.DB});P.prototype.get_m_engineForce=P.prototype.iD=function(){return aw(this.kB)};P.prototype.set_m_engineForce=P.prototype.$F=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);bw(c,a)};Object.defineProperty(P.prototype,"m_engineForce",{get:P.prototype.iD,set:P.prototype.$F}); +P.prototype.get_m_rollInfluence=P.prototype.cE=function(){return cw(this.kB)};P.prototype.set_m_rollInfluence=P.prototype.VG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);dw(c,a)};Object.defineProperty(P.prototype,"m_rollInfluence",{get:P.prototype.cE,set:P.prototype.VG});P.prototype.get_m_suspensionRestLength1=P.prototype.rE=function(){return ew(this.kB)};P.prototype.set_m_suspensionRestLength1=P.prototype.jH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fw(c,a)}; +Object.defineProperty(P.prototype,"m_suspensionRestLength1",{get:P.prototype.rE,set:P.prototype.jH});P.prototype.get_m_wheelsRadius=P.prototype.FE=function(){return gw(this.kB)};P.prototype.set_m_wheelsRadius=P.prototype.xH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hw(c,a)};Object.defineProperty(P.prototype,"m_wheelsRadius",{get:P.prototype.FE,set:P.prototype.xH});P.prototype.get_m_wheelsDampingCompression=P.prototype.RB=function(){return iw(this.kB)}; +P.prototype.set_m_wheelsDampingCompression=P.prototype.aC=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jw(c,a)};Object.defineProperty(P.prototype,"m_wheelsDampingCompression",{get:P.prototype.RB,set:P.prototype.aC});P.prototype.get_m_wheelsDampingRelaxation=P.prototype.SB=function(){return kw(this.kB)};P.prototype.set_m_wheelsDampingRelaxation=P.prototype.bC=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);lw(c,a)}; +Object.defineProperty(P.prototype,"m_wheelsDampingRelaxation",{get:P.prototype.SB,set:P.prototype.bC});P.prototype.get_m_steering=P.prototype.kE=function(){return mw(this.kB)};P.prototype.set_m_steering=P.prototype.cH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);nw(c,a)};Object.defineProperty(P.prototype,"m_steering",{get:P.prototype.kE,set:P.prototype.cH});P.prototype.get_m_maxSuspensionForce=P.prototype.zB=function(){return ow(this.kB)}; +P.prototype.set_m_maxSuspensionForce=P.prototype.GB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pw(c,a)};Object.defineProperty(P.prototype,"m_maxSuspensionForce",{get:P.prototype.zB,set:P.prototype.GB});P.prototype.get_m_maxSuspensionTravelCm=P.prototype.AB=function(){return qw(this.kB)};P.prototype.set_m_maxSuspensionTravelCm=P.prototype.HB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);rw(c,a)}; +Object.defineProperty(P.prototype,"m_maxSuspensionTravelCm",{get:P.prototype.AB,set:P.prototype.HB});P.prototype.get_m_wheelsSuspensionForce=P.prototype.GE=function(){return sw(this.kB)};P.prototype.set_m_wheelsSuspensionForce=P.prototype.yH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);tw(c,a)};Object.defineProperty(P.prototype,"m_wheelsSuspensionForce",{get:P.prototype.GE,set:P.prototype.yH});P.prototype.get_m_bIsFrontWheel=P.prototype.JB=function(){return!!uw(this.kB)}; +P.prototype.set_m_bIsFrontWheel=P.prototype.TB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vw(c,a)};Object.defineProperty(P.prototype,"m_bIsFrontWheel",{get:P.prototype.JB,set:P.prototype.TB});P.prototype.get_m_raycastInfo=P.prototype.aE=function(){return k(ww(this.kB),N)};P.prototype.set_m_raycastInfo=P.prototype.TG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xw(c,a)};Object.defineProperty(P.prototype,"m_raycastInfo",{get:P.prototype.aE,set:P.prototype.TG}); +P.prototype.get_m_chassisConnectionPointCS=P.prototype.SC=function(){return k(yw(this.kB),m)};P.prototype.set_m_chassisConnectionPointCS=P.prototype.JF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zw(c,a)};Object.defineProperty(P.prototype,"m_chassisConnectionPointCS",{get:P.prototype.SC,set:P.prototype.JF});P.prototype.get_m_worldTransform=P.prototype.HE=function(){return k(Aw(this.kB),r)}; +P.prototype.set_m_worldTransform=P.prototype.zH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Bw(c,a)};Object.defineProperty(P.prototype,"m_worldTransform",{get:P.prototype.HE,set:P.prototype.zH});P.prototype.get_m_wheelDirectionCS=P.prototype.QB=function(){return k(Cw(this.kB),m)};P.prototype.set_m_wheelDirectionCS=P.prototype.$B=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dw(c,a)};Object.defineProperty(P.prototype,"m_wheelDirectionCS",{get:P.prototype.QB,set:P.prototype.$B}); +P.prototype.get_m_wheelAxleCS=P.prototype.PB=function(){return k(Ew(this.kB),m)};P.prototype.set_m_wheelAxleCS=P.prototype.ZB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Fw(c,a)};Object.defineProperty(P.prototype,"m_wheelAxleCS",{get:P.prototype.PB,set:P.prototype.ZB});P.prototype.get_m_rotation=P.prototype.eE=function(){return Gw(this.kB)};P.prototype.set_m_rotation=P.prototype.XG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Hw(c,a)}; +Object.defineProperty(P.prototype,"m_rotation",{get:P.prototype.eE,set:P.prototype.XG});P.prototype.get_m_deltaRotation=P.prototype.bD=function(){return Iw(this.kB)};P.prototype.set_m_deltaRotation=P.prototype.TF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jw(c,a)};Object.defineProperty(P.prototype,"m_deltaRotation",{get:P.prototype.bD,set:P.prototype.TF});P.prototype.get_m_brake=P.prototype.LC=function(){return Kw(this.kB)}; +P.prototype.set_m_brake=P.prototype.CF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Lw(c,a)};Object.defineProperty(P.prototype,"m_brake",{get:P.prototype.LC,set:P.prototype.CF});P.prototype.get_m_clippedInvContactDotSuspension=P.prototype.TC=function(){return Mw(this.kB)};P.prototype.set_m_clippedInvContactDotSuspension=P.prototype.KF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Nw(c,a)}; +Object.defineProperty(P.prototype,"m_clippedInvContactDotSuspension",{get:P.prototype.TC,set:P.prototype.KF});P.prototype.get_m_suspensionRelativeVelocity=P.prototype.pE=function(){return Ow(this.kB)};P.prototype.set_m_suspensionRelativeVelocity=P.prototype.hH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Pw(c,a)};Object.defineProperty(P.prototype,"m_suspensionRelativeVelocity",{get:P.prototype.pE,set:P.prototype.hH});P.prototype.get_m_skidInfo=P.prototype.hE=function(){return Qw(this.kB)}; +P.prototype.set_m_skidInfo=P.prototype.$G=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Rw(c,a)};Object.defineProperty(P.prototype,"m_skidInfo",{get:P.prototype.hE,set:P.prototype.$G});P.prototype.__destroy__=function(){Sw(this.kB)};function zF(a,c,d,e){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);this.kB=void 0===e?Tw(a,c,d):Uw(a,c,d,e);h(zF)[this.kB]=this}zF.prototype=Object.create(rE.prototype); +zF.prototype.constructor=zF;zF.prototype.lB=zF;zF.mB={};b.btKinematicCharacterController=zF;zF.prototype.setUpAxis=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vw(c,a)};zF.prototype.setWalkDirection=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ww(c,a)};zF.prototype.setVelocityForTimeInterval=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Xw(d,a,c)}; +zF.prototype.warp=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Yw(c,a)};zF.prototype.preStep=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Zw(c,a)};zF.prototype.playerStep=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);$w(d,a,c)};zF.prototype.setFallSpeed=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ax(c,a)};zF.prototype.setJumpSpeed=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);bx(c,a)}; +zF.prototype.setMaxJumpHeight=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);cx(c,a)};zF.prototype.canJump=function(){return!!dx(this.kB)};zF.prototype.jump=function(){ex(this.kB)};zF.prototype.setGravity=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fx(c,a)};zF.prototype.getGravity=function(){return gx(this.kB)};zF.prototype.setMaxSlope=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hx(c,a)};zF.prototype.getMaxSlope=function(){return ix(this.kB)}; +zF.prototype.getGhostObject=function(){return k(jx(this.kB),Q)};zF.prototype.setUseGhostSweepTest=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);kx(c,a)};zF.prototype.onGround=function(){return!!lx(this.kB)};zF.prototype.setUpInterpolate=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);mx(c,a)};zF.prototype.updateAction=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);nx(d,a,c)};zF.prototype.__destroy__=function(){ox(this.kB)}; +function R(a,c,d){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);this.kB=px(a,c,d);h(R)[this.kB]=this}R.prototype=Object.create(rE.prototype);R.prototype.constructor=R;R.prototype.lB=R;R.mB={};b.btRaycastVehicle=R;R.prototype.applyEngineForce=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);qx(d,a,c)}; +R.prototype.setSteeringValue=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);rx(d,a,c)};R.prototype.getWheelTransformWS=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(sx(c,a),r)};R.prototype.updateWheelTransform=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);tx(d,a,c)}; +R.prototype.addWheel=function(a,c,d,e,g,n,z){var T=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);n&&"object"===typeof n&&(n=n.kB);z&&"object"===typeof z&&(z=z.kB);return k(ux(T,a,c,d,e,g,n,z),P)};R.prototype.getNumWheels=function(){return vx(this.kB)};R.prototype.getRigidBody=function(){return k(wx(this.kB),K)}; +R.prototype.getWheelInfo=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(xx(c,a),P)};R.prototype.setBrake=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);yx(d,a,c)};R.prototype.setCoordinateSystem=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);zx(e,a,c,d)};R.prototype.getCurrentSpeedKmHour=function(){return Ax(this.kB)}; +R.prototype.getChassisWorldTransform=function(){return k(Bx(this.kB),r)};R.prototype.rayCast=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return Cx(c,a)};R.prototype.updateVehicle=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dx(c,a)};R.prototype.resetSuspension=function(){Ex(this.kB)};R.prototype.getSteeringValue=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return Fx(c,a)}; +R.prototype.updateWheelTransformsWS=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);void 0===c?Gx(d,a):Hx(d,a,c)};R.prototype.setPitchControl=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ix(c,a)};R.prototype.updateSuspension=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jx(c,a)};R.prototype.updateFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Kx(c,a)};R.prototype.getRightAxis=function(){return Lx(this.kB)}; +R.prototype.getUpAxis=function(){return Mx(this.kB)};R.prototype.getForwardAxis=function(){return Nx(this.kB)};R.prototype.getForwardVector=function(){return k(Ox(this.kB),m)};R.prototype.getUserConstraintType=function(){return Px(this.kB)};R.prototype.setUserConstraintType=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Qx(c,a)};R.prototype.setUserConstraintId=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Rx(c,a)};R.prototype.getUserConstraintId=function(){return Sx(this.kB)}; +R.prototype.updateAction=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Tx(d,a,c)};R.prototype.__destroy__=function(){Ux(this.kB)};function Q(){this.kB=Vx();h(Q)[this.kB]=this}Q.prototype=Object.create(y.prototype);Q.prototype.constructor=Q;Q.prototype.lB=Q;Q.mB={};b.btPairCachingGhostObject=Q;Q.prototype.setAnisotropicFriction=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Wx(d,a,c)}; +Q.prototype.getCollisionShape=function(){return k(Xx(this.kB),l)};Q.prototype.setContactProcessingThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Yx(c,a)};Q.prototype.setActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Zx(c,a)};Q.prototype.forceActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$x(c,a)};Q.prototype.activate=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);void 0===a?ay(c):by(c,a)}; +Q.prototype.isActive=function(){return!!cy(this.kB)};Q.prototype.isKinematicObject=function(){return!!dy(this.kB)};Q.prototype.isStaticObject=function(){return!!ey(this.kB)};Q.prototype.isStaticOrKinematicObject=function(){return!!fy(this.kB)};Q.prototype.getRestitution=function(){return gy(this.kB)};Q.prototype.getFriction=function(){return hy(this.kB)};Q.prototype.getRollingFriction=function(){return iy(this.kB)}; +Q.prototype.setRestitution=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jy(c,a)};Q.prototype.setFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ky(c,a)};Q.prototype.setRollingFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ly(c,a)};Q.prototype.getWorldTransform=function(){return k(my(this.kB),r)};Q.prototype.getCollisionFlags=function(){return ny(this.kB)}; +Q.prototype.setCollisionFlags=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);oy(c,a)};Q.prototype.setWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);py(c,a)};Q.prototype.setCollisionShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);qy(c,a)};Q.prototype.setCcdMotionThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ry(c,a)};Q.prototype.setCcdSweptSphereRadius=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);sy(c,a)}; +Q.prototype.getUserIndex=function(){return ty(this.kB)};Q.prototype.setUserIndex=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);uy(c,a)};Q.prototype.getUserPointer=function(){return k(vy(this.kB),XD)};Q.prototype.setUserPointer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);wy(c,a)};Q.prototype.getBroadphaseHandle=function(){return k(xy(this.kB),YD)};Q.prototype.getNumOverlappingObjects=function(){return yy(this.kB)}; +Q.prototype.getOverlappingObject=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(zy(c,a),q)};Q.prototype.__destroy__=function(){Ay(this.kB)};function AF(){this.kB=By();h(AF)[this.kB]=this}AF.prototype=Object.create(f.prototype);AF.prototype.constructor=AF;AF.prototype.lB=AF;AF.mB={};b.btGhostPairCallback=AF;AF.prototype.__destroy__=function(){Cy(this.kB)};function S(){this.kB=Dy();h(S)[this.kB]=this}S.prototype=Object.create(f.prototype);S.prototype.constructor=S; +S.prototype.lB=S;S.mB={};b.btSoftBodyWorldInfo=S;S.prototype.get_air_density=S.prototype.cC=function(){return Ey(this.kB)};S.prototype.set_air_density=S.prototype.UE=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Fy(c,a)};Object.defineProperty(S.prototype,"air_density",{get:S.prototype.cC,set:S.prototype.UE});S.prototype.get_water_density=S.prototype.RE=function(){return Gy(this.kB)}; +S.prototype.set_water_density=S.prototype.JH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Hy(c,a)};Object.defineProperty(S.prototype,"water_density",{get:S.prototype.RE,set:S.prototype.JH});S.prototype.get_water_offset=S.prototype.TE=function(){return Iy(this.kB)};S.prototype.set_water_offset=S.prototype.LH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jy(c,a)};Object.defineProperty(S.prototype,"water_offset",{get:S.prototype.TE,set:S.prototype.LH}); +S.prototype.get_m_maxDisplacement=S.prototype.ND=function(){return Ky(this.kB)};S.prototype.set_m_maxDisplacement=S.prototype.EG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ly(c,a)};Object.defineProperty(S.prototype,"m_maxDisplacement",{get:S.prototype.ND,set:S.prototype.EG});S.prototype.get_water_normal=S.prototype.SE=function(){return k(My(this.kB),m)};S.prototype.set_water_normal=S.prototype.KH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ny(c,a)}; +Object.defineProperty(S.prototype,"water_normal",{get:S.prototype.SE,set:S.prototype.KH});S.prototype.get_m_broadphase=S.prototype.MC=function(){return k(Oy(this.kB),VD)};S.prototype.set_m_broadphase=S.prototype.DF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Py(c,a)};Object.defineProperty(S.prototype,"m_broadphase",{get:S.prototype.MC,set:S.prototype.DF});S.prototype.get_m_dispatcher=S.prototype.dD=function(){return k(Qy(this.kB),TD)}; +S.prototype.set_m_dispatcher=S.prototype.VF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ry(c,a)};Object.defineProperty(S.prototype,"m_dispatcher",{get:S.prototype.dD,set:S.prototype.VF});S.prototype.get_m_gravity=S.prototype.mD=function(){return k(Sy(this.kB),m)};S.prototype.set_m_gravity=S.prototype.dG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Ty(c,a)};Object.defineProperty(S.prototype,"m_gravity",{get:S.prototype.mD,set:S.prototype.dG});S.prototype.__destroy__=function(){Uy(this.kB)}; +function U(){throw"cannot construct a Face, no constructor in IDL";}U.prototype=Object.create(f.prototype);U.prototype.constructor=U;U.prototype.lB=U;U.mB={};b.Face=U;U.prototype.get_m_n=U.prototype.MB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Vy(c,a),Node)};U.prototype.set_m_n=U.prototype.WB=function(a,c){var d=this.kB;ND();a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);Wy(d,a,c)};Object.defineProperty(U.prototype,"m_n",{get:U.prototype.MB,set:U.prototype.WB}); +U.prototype.get_m_normal=U.prototype.RD=function(){return k(Xy(this.kB),m)};U.prototype.set_m_normal=U.prototype.IG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Yy(c,a)};Object.defineProperty(U.prototype,"m_normal",{get:U.prototype.RD,set:U.prototype.IG});U.prototype.get_m_ra=U.prototype.$D=function(){return Zy(this.kB)};U.prototype.set_m_ra=U.prototype.SG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$y(c,a)}; +Object.defineProperty(U.prototype,"m_ra",{get:U.prototype.$D,set:U.prototype.SG});U.prototype.__destroy__=function(){az(this.kB)};function BF(){throw"cannot construct a tFaceArray, no constructor in IDL";}BF.prototype=Object.create(f.prototype);BF.prototype.constructor=BF;BF.prototype.lB=BF;BF.mB={};b.tFaceArray=BF;BF.prototype.size=BF.prototype.size=function(){return bz(this.kB)};BF.prototype.at=BF.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(cz(c,a),U)}; +BF.prototype.__destroy__=function(){dz(this.kB)};function Node(){throw"cannot construct a Node, no constructor in IDL";}Node.prototype=Object.create(f.prototype);Node.prototype.constructor=Node;Node.prototype.lB=Node;Node.mB={};b.Node=Node;Node.prototype.get_m_x=Node.prototype.IE=function(){return k(ez(this.kB),m)};Node.prototype.set_m_x=Node.prototype.AH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fz(c,a)};Object.defineProperty(Node.prototype,"m_x",{get:Node.prototype.IE,set:Node.prototype.AH}); +Node.prototype.get_m_q=Node.prototype.ZD=function(){return k(gz(this.kB),m)};Node.prototype.set_m_q=Node.prototype.QG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hz(c,a)};Object.defineProperty(Node.prototype,"m_q",{get:Node.prototype.ZD,set:Node.prototype.QG});Node.prototype.get_m_v=Node.prototype.AE=function(){return k(iz(this.kB),m)};Node.prototype.set_m_v=Node.prototype.sH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jz(c,a)}; +Object.defineProperty(Node.prototype,"m_v",{get:Node.prototype.AE,set:Node.prototype.sH});Node.prototype.get_m_f=Node.prototype.jD=function(){return k(kz(this.kB),m)};Node.prototype.set_m_f=Node.prototype.aG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);lz(c,a)};Object.defineProperty(Node.prototype,"m_f",{get:Node.prototype.jD,set:Node.prototype.aG});Node.prototype.get_m_n=Node.prototype.MB=function(){return k(mz(this.kB),m)}; +Node.prototype.set_m_n=Node.prototype.WB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);nz(c,a)};Object.defineProperty(Node.prototype,"m_n",{get:Node.prototype.MB,set:Node.prototype.WB});Node.prototype.get_m_im=Node.prototype.vD=function(){return oz(this.kB)};Node.prototype.set_m_im=Node.prototype.mG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pz(c,a)};Object.defineProperty(Node.prototype,"m_im",{get:Node.prototype.vD,set:Node.prototype.mG}); +Node.prototype.get_m_area=Node.prototype.JC=function(){return qz(this.kB)};Node.prototype.set_m_area=Node.prototype.AF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);rz(c,a)};Object.defineProperty(Node.prototype,"m_area",{get:Node.prototype.JC,set:Node.prototype.AF});Node.prototype.__destroy__=function(){sz(this.kB)};function CF(){throw"cannot construct a tNodeArray, no constructor in IDL";}CF.prototype=Object.create(f.prototype);CF.prototype.constructor=CF;CF.prototype.lB=CF;CF.mB={}; +b.tNodeArray=CF;CF.prototype.size=CF.prototype.size=function(){return tz(this.kB)};CF.prototype.at=CF.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(uz(c,a),Node)};CF.prototype.__destroy__=function(){vz(this.kB)};function V(){throw"cannot construct a Material, no constructor in IDL";}V.prototype=Object.create(f.prototype);V.prototype.constructor=V;V.prototype.lB=V;V.mB={};b.Material=V;V.prototype.get_m_kLST=V.prototype.BD=function(){return wz(this.kB)}; +V.prototype.set_m_kLST=V.prototype.sG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xz(c,a)};Object.defineProperty(V.prototype,"m_kLST",{get:V.prototype.BD,set:V.prototype.sG});V.prototype.get_m_kAST=V.prototype.AD=function(){return yz(this.kB)};V.prototype.set_m_kAST=V.prototype.rG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zz(c,a)};Object.defineProperty(V.prototype,"m_kAST",{get:V.prototype.AD,set:V.prototype.rG});V.prototype.get_m_kVST=V.prototype.CD=function(){return Az(this.kB)}; +V.prototype.set_m_kVST=V.prototype.tG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Bz(c,a)};Object.defineProperty(V.prototype,"m_kVST",{get:V.prototype.CD,set:V.prototype.tG});V.prototype.get_m_flags=V.prototype.tB=function(){return Cz(this.kB)};V.prototype.set_m_flags=V.prototype.uB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Dz(c,a)};Object.defineProperty(V.prototype,"m_flags",{get:V.prototype.tB,set:V.prototype.uB});V.prototype.__destroy__=function(){Ez(this.kB)}; +function DF(){throw"cannot construct a tMaterialArray, no constructor in IDL";}DF.prototype=Object.create(f.prototype);DF.prototype.constructor=DF;DF.prototype.lB=DF;DF.mB={};b.tMaterialArray=DF;DF.prototype.size=DF.prototype.size=function(){return Fz(this.kB)};DF.prototype.at=DF.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Gz(c,a),V)};DF.prototype.__destroy__=function(){Hz(this.kB)};function W(){throw"cannot construct a Anchor, no constructor in IDL";} +W.prototype=Object.create(f.prototype);W.prototype.constructor=W;W.prototype.lB=W;W.mB={};b.Anchor=W;W.prototype.get_m_node=W.prototype.PD=function(){return k(Iz(this.kB),Node)};W.prototype.set_m_node=W.prototype.GG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Jz(c,a)};Object.defineProperty(W.prototype,"m_node",{get:W.prototype.PD,set:W.prototype.GG});W.prototype.get_m_local=W.prototype.FD=function(){return k(Kz(this.kB),m)}; +W.prototype.set_m_local=W.prototype.wG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Lz(c,a)};Object.defineProperty(W.prototype,"m_local",{get:W.prototype.FD,set:W.prototype.wG});W.prototype.get_m_body=W.prototype.KC=function(){return k(Mz(this.kB),K)};W.prototype.set_m_body=W.prototype.BF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Nz(c,a)};Object.defineProperty(W.prototype,"m_body",{get:W.prototype.KC,set:W.prototype.BF});W.prototype.get_m_influence=W.prototype.yD=function(){return Oz(this.kB)}; +W.prototype.set_m_influence=W.prototype.pG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Pz(c,a)};Object.defineProperty(W.prototype,"m_influence",{get:W.prototype.yD,set:W.prototype.pG});W.prototype.get_m_c0=W.prototype.NC=function(){return k(Qz(this.kB),uE)};W.prototype.set_m_c0=W.prototype.EF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Rz(c,a)};Object.defineProperty(W.prototype,"m_c0",{get:W.prototype.NC,set:W.prototype.EF}); +W.prototype.get_m_c1=W.prototype.OC=function(){return k(Sz(this.kB),m)};W.prototype.set_m_c1=W.prototype.FF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Tz(c,a)};Object.defineProperty(W.prototype,"m_c1",{get:W.prototype.OC,set:W.prototype.FF});W.prototype.get_m_c2=W.prototype.PC=function(){return Uz(this.kB)};W.prototype.set_m_c2=W.prototype.GF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);Vz(c,a)};Object.defineProperty(W.prototype,"m_c2",{get:W.prototype.PC,set:W.prototype.GF}); +W.prototype.__destroy__=function(){Wz(this.kB)};function EF(){throw"cannot construct a tAnchorArray, no constructor in IDL";}EF.prototype=Object.create(f.prototype);EF.prototype.constructor=EF;EF.prototype.lB=EF;EF.mB={};b.tAnchorArray=EF;EF.prototype.size=EF.prototype.size=function(){return Xz(this.kB)};EF.prototype.at=EF.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(Yz(c,a),W)};EF.prototype.clear=EF.prototype.clear=function(){Zz(this.kB)}; +EF.prototype.push_back=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$z(c,a)};EF.prototype.pop_back=function(){aA(this.kB)};EF.prototype.__destroy__=function(){bA(this.kB)};function X(){throw"cannot construct a Config, no constructor in IDL";}X.prototype=Object.create(f.prototype);X.prototype.constructor=X;X.prototype.lB=X;X.mB={};b.Config=X;X.prototype.get_kVCF=X.prototype.zC=function(){return cA(this.kB)}; +X.prototype.set_kVCF=X.prototype.qF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);dA(c,a)};Object.defineProperty(X.prototype,"kVCF",{get:X.prototype.zC,set:X.prototype.qF});X.prototype.get_kDP=X.prototype.mC=function(){return eA(this.kB)};X.prototype.set_kDP=X.prototype.dF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);fA(c,a)};Object.defineProperty(X.prototype,"kDP",{get:X.prototype.mC,set:X.prototype.dF});X.prototype.get_kDG=X.prototype.lC=function(){return gA(this.kB)}; +X.prototype.set_kDG=X.prototype.cF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);hA(c,a)};Object.defineProperty(X.prototype,"kDG",{get:X.prototype.lC,set:X.prototype.cF});X.prototype.get_kLF=X.prototype.oC=function(){return iA(this.kB)};X.prototype.set_kLF=X.prototype.fF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);jA(c,a)};Object.defineProperty(X.prototype,"kLF",{get:X.prototype.oC,set:X.prototype.fF});X.prototype.get_kPR=X.prototype.qC=function(){return kA(this.kB)}; +X.prototype.set_kPR=X.prototype.hF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);lA(c,a)};Object.defineProperty(X.prototype,"kPR",{get:X.prototype.qC,set:X.prototype.hF});X.prototype.get_kVC=X.prototype.yC=function(){return mA(this.kB)};X.prototype.set_kVC=X.prototype.pF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);nA(c,a)};Object.defineProperty(X.prototype,"kVC",{get:X.prototype.yC,set:X.prototype.pF});X.prototype.get_kDF=X.prototype.kC=function(){return oA(this.kB)}; +X.prototype.set_kDF=X.prototype.bF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pA(c,a)};Object.defineProperty(X.prototype,"kDF",{get:X.prototype.kC,set:X.prototype.bF});X.prototype.get_kMT=X.prototype.pC=function(){return qA(this.kB)};X.prototype.set_kMT=X.prototype.gF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);rA(c,a)};Object.defineProperty(X.prototype,"kMT",{get:X.prototype.pC,set:X.prototype.gF});X.prototype.get_kCHR=X.prototype.jC=function(){return sA(this.kB)}; +X.prototype.set_kCHR=X.prototype.aF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);tA(c,a)};Object.defineProperty(X.prototype,"kCHR",{get:X.prototype.jC,set:X.prototype.aF});X.prototype.get_kKHR=X.prototype.nC=function(){return uA(this.kB)};X.prototype.set_kKHR=X.prototype.eF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);vA(c,a)};Object.defineProperty(X.prototype,"kKHR",{get:X.prototype.nC,set:X.prototype.eF});X.prototype.get_kSHR=X.prototype.rC=function(){return wA(this.kB)}; +X.prototype.set_kSHR=X.prototype.iF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xA(c,a)};Object.defineProperty(X.prototype,"kSHR",{get:X.prototype.rC,set:X.prototype.iF});X.prototype.get_kAHR=X.prototype.iC=function(){return yA(this.kB)};X.prototype.set_kAHR=X.prototype.$E=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);zA(c,a)};Object.defineProperty(X.prototype,"kAHR",{get:X.prototype.iC,set:X.prototype.$E});X.prototype.get_kSRHR_CL=X.prototype.uC=function(){return AA(this.kB)}; +X.prototype.set_kSRHR_CL=X.prototype.lF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);BA(c,a)};Object.defineProperty(X.prototype,"kSRHR_CL",{get:X.prototype.uC,set:X.prototype.lF});X.prototype.get_kSKHR_CL=X.prototype.sC=function(){return CA(this.kB)};X.prototype.set_kSKHR_CL=X.prototype.jF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);DA(c,a)};Object.defineProperty(X.prototype,"kSKHR_CL",{get:X.prototype.sC,set:X.prototype.jF});X.prototype.get_kSSHR_CL=X.prototype.wC=function(){return EA(this.kB)}; +X.prototype.set_kSSHR_CL=X.prototype.nF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);FA(c,a)};Object.defineProperty(X.prototype,"kSSHR_CL",{get:X.prototype.wC,set:X.prototype.nF});X.prototype.get_kSR_SPLT_CL=X.prototype.vC=function(){return GA(this.kB)};X.prototype.set_kSR_SPLT_CL=X.prototype.mF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);HA(c,a)};Object.defineProperty(X.prototype,"kSR_SPLT_CL",{get:X.prototype.vC,set:X.prototype.mF}); +X.prototype.get_kSK_SPLT_CL=X.prototype.tC=function(){return IA(this.kB)};X.prototype.set_kSK_SPLT_CL=X.prototype.kF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);JA(c,a)};Object.defineProperty(X.prototype,"kSK_SPLT_CL",{get:X.prototype.tC,set:X.prototype.kF});X.prototype.get_kSS_SPLT_CL=X.prototype.xC=function(){return KA(this.kB)};X.prototype.set_kSS_SPLT_CL=X.prototype.oF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);LA(c,a)}; +Object.defineProperty(X.prototype,"kSS_SPLT_CL",{get:X.prototype.xC,set:X.prototype.oF});X.prototype.get_maxvolume=X.prototype.JE=function(){return MA(this.kB)};X.prototype.set_maxvolume=X.prototype.BH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);NA(c,a)};Object.defineProperty(X.prototype,"maxvolume",{get:X.prototype.JE,set:X.prototype.BH});X.prototype.get_timescale=X.prototype.OE=function(){return OA(this.kB)}; +X.prototype.set_timescale=X.prototype.GH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);PA(c,a)};Object.defineProperty(X.prototype,"timescale",{get:X.prototype.OE,set:X.prototype.GH});X.prototype.get_viterations=X.prototype.QE=function(){return QA(this.kB)};X.prototype.set_viterations=X.prototype.IH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);RA(c,a)};Object.defineProperty(X.prototype,"viterations",{get:X.prototype.QE,set:X.prototype.IH}); +X.prototype.get_piterations=X.prototype.ME=function(){return SA(this.kB)};X.prototype.set_piterations=X.prototype.EH=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);TA(c,a)};Object.defineProperty(X.prototype,"piterations",{get:X.prototype.ME,set:X.prototype.EH});X.prototype.get_diterations=X.prototype.fC=function(){return UA(this.kB)};X.prototype.set_diterations=X.prototype.XE=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);VA(c,a)}; +Object.defineProperty(X.prototype,"diterations",{get:X.prototype.fC,set:X.prototype.XE});X.prototype.get_citerations=X.prototype.dC=function(){return WA(this.kB)};X.prototype.set_citerations=X.prototype.VE=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);XA(c,a)};Object.defineProperty(X.prototype,"citerations",{get:X.prototype.dC,set:X.prototype.VE});X.prototype.get_collisions=X.prototype.eC=function(){return YA(this.kB)}; +X.prototype.set_collisions=X.prototype.WE=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);ZA(c,a)};Object.defineProperty(X.prototype,"collisions",{get:X.prototype.eC,set:X.prototype.WE});X.prototype.__destroy__=function(){$A(this.kB)};function Y(a,c,d,e){ND();a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);"object"==typeof e&&(e=RD(e));this.kB=aB(a,c,d,e);h(Y)[this.kB]=this}Y.prototype=Object.create(q.prototype); +Y.prototype.constructor=Y;Y.prototype.lB=Y;Y.mB={};b.btSoftBody=Y;Y.prototype.checkLink=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return!!bB(d,a,c)};Y.prototype.checkFace=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);return!!cB(e,a,c,d)};Y.prototype.appendMaterial=function(){return k(dB(this.kB),V)}; +Y.prototype.appendNode=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);eB(d,a,c)};Y.prototype.appendLink=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);fB(g,a,c,d,e)}; +Y.prototype.appendFace=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);gB(g,a,c,d,e)};Y.prototype.appendTetra=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);hB(n,a,c,d,e,g)}; +Y.prototype.appendAnchor=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);iB(g,a,c,d,e)};Y.prototype.addForce=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);void 0===c?jB(d,a):kB(d,a,c)};Y.prototype.addAeroForceToNode=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);lB(d,a,c)}; +Y.prototype.getTotalMass=function(){return mB(this.kB)};Y.prototype.setTotalMass=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);nB(d,a,c)};Y.prototype.setMass=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);oB(d,a,c)};Y.prototype.transform=Y.prototype.transform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);pB(c,a)}; +Y.prototype.translate=Y.prototype.translate=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);qB(c,a)};Y.prototype.rotate=Y.prototype.rotate=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);rB(c,a)};Y.prototype.scale=Y.prototype.scale=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);sB(c,a)};Y.prototype.generateClusters=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return void 0===c?tB(d,a):uB(d,a,c)}; +Y.prototype.generateBendingConstraints=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);return vB(d,a,c)};Y.prototype.upcast=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(wB(c,a),Y)};Y.prototype.getRestLengthScale=function(){return xB(this.kB)};Y.prototype.setRestLengthScale=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);yB(c,a)}; +Y.prototype.setAnisotropicFriction=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);zB(d,a,c)};Y.prototype.getCollisionShape=function(){return k(AB(this.kB),l)};Y.prototype.setContactProcessingThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);BB(c,a)};Y.prototype.setActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);CB(c,a)}; +Y.prototype.forceActivationState=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);DB(c,a)};Y.prototype.activate=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);void 0===a?EB(c):FB(c,a)};Y.prototype.isActive=function(){return!!GB(this.kB)};Y.prototype.isKinematicObject=function(){return!!HB(this.kB)};Y.prototype.isStaticObject=function(){return!!IB(this.kB)};Y.prototype.isStaticOrKinematicObject=function(){return!!JB(this.kB)};Y.prototype.getRestitution=function(){return KB(this.kB)}; +Y.prototype.getFriction=function(){return LB(this.kB)};Y.prototype.getRollingFriction=function(){return MB(this.kB)};Y.prototype.setRestitution=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);NB(c,a)};Y.prototype.setFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);OB(c,a)};Y.prototype.setRollingFriction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);PB(c,a)};Y.prototype.getWorldTransform=function(){return k(QB(this.kB),r)};Y.prototype.getCollisionFlags=function(){return RB(this.kB)}; +Y.prototype.setCollisionFlags=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);SB(c,a)};Y.prototype.setWorldTransform=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);TB(c,a)};Y.prototype.setCollisionShape=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);UB(c,a)};Y.prototype.setCcdMotionThreshold=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);VB(c,a)};Y.prototype.setCcdSweptSphereRadius=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);WB(c,a)}; +Y.prototype.getUserIndex=function(){return XB(this.kB)};Y.prototype.setUserIndex=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);YB(c,a)};Y.prototype.getUserPointer=function(){return k(ZB(this.kB),XD)};Y.prototype.setUserPointer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);$B(c,a)};Y.prototype.getBroadphaseHandle=function(){return k(aC(this.kB),YD)};Y.prototype.get_m_cfg=Y.prototype.QC=function(){return k(bC(this.kB),X)}; +Y.prototype.set_m_cfg=Y.prototype.HF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);cC(c,a)};Object.defineProperty(Y.prototype,"m_cfg",{get:Y.prototype.QC,set:Y.prototype.HF});Y.prototype.get_m_nodes=Y.prototype.QD=function(){return k(dC(this.kB),CF)};Y.prototype.set_m_nodes=Y.prototype.HG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);eC(c,a)};Object.defineProperty(Y.prototype,"m_nodes",{get:Y.prototype.QD,set:Y.prototype.HG}); +Y.prototype.get_m_faces=Y.prototype.KB=function(){return k(fC(this.kB),BF)};Y.prototype.set_m_faces=Y.prototype.UB=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);gC(c,a)};Object.defineProperty(Y.prototype,"m_faces",{get:Y.prototype.KB,set:Y.prototype.UB});Y.prototype.get_m_materials=Y.prototype.MD=function(){return k(hC(this.kB),DF)};Y.prototype.set_m_materials=Y.prototype.DG=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);iC(c,a)}; +Object.defineProperty(Y.prototype,"m_materials",{get:Y.prototype.MD,set:Y.prototype.DG});Y.prototype.get_m_anchors=Y.prototype.GC=function(){return k(jC(this.kB),EF)};Y.prototype.set_m_anchors=Y.prototype.xF=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);kC(c,a)};Object.defineProperty(Y.prototype,"m_anchors",{get:Y.prototype.GC,set:Y.prototype.xF});Y.prototype.__destroy__=function(){lC(this.kB)}; +function FF(a){a&&"object"===typeof a&&(a=a.kB);this.kB=void 0===a?mC():nC(a);h(FF)[this.kB]=this}FF.prototype=Object.create(nE.prototype);FF.prototype.constructor=FF;FF.prototype.lB=FF;FF.mB={};b.btSoftBodyRigidBodyCollisionConfiguration=FF;FF.prototype.__destroy__=function(){oC(this.kB)};function GF(){this.kB=pC();h(GF)[this.kB]=this}GF.prototype=Object.create(sE.prototype);GF.prototype.constructor=GF;GF.prototype.lB=GF;GF.mB={};b.btDefaultSoftBodySolver=GF;GF.prototype.__destroy__=function(){qC(this.kB)}; +function HF(){throw"cannot construct a btSoftBodyArray, no constructor in IDL";}HF.prototype=Object.create(f.prototype);HF.prototype.constructor=HF;HF.prototype.lB=HF;HF.mB={};b.btSoftBodyArray=HF;HF.prototype.size=HF.prototype.size=function(){return rC(this.kB)};HF.prototype.at=HF.prototype.at=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);return k(sC(c,a),Y)};HF.prototype.__destroy__=function(){tC(this.kB)}; +function Z(a,c,d,e,g){a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);this.kB=uC(a,c,d,e,g);h(Z)[this.kB]=this}Z.prototype=Object.create(x.prototype);Z.prototype.constructor=Z;Z.prototype.lB=Z;Z.mB={};b.btSoftRigidDynamicsWorld=Z; +Z.prototype.addSoftBody=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);vC(e,a,c,d)};Z.prototype.removeSoftBody=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);wC(c,a)};Z.prototype.removeCollisionObject=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);xC(c,a)};Z.prototype.getWorldInfo=function(){return k(yC(this.kB),S)};Z.prototype.getSoftBodyArray=function(){return k(zC(this.kB),HF)}; +Z.prototype.getDispatcher=function(){return k(AC(this.kB),TD)};Z.prototype.rayTest=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);BC(e,a,c,d)};Z.prototype.getPairCache=function(){return k(CC(this.kB),UD)};Z.prototype.getDispatchInfo=function(){return k(DC(this.kB),p)}; +Z.prototype.addCollisionObject=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?EC(e,a):void 0===d?FC(e,a,c):GC(e,a,c,d)};Z.prototype.getBroadphase=function(){return k(HC(this.kB),VD)}; +Z.prototype.convexSweepTest=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);IC(n,a,c,d,e,g)};Z.prototype.contactPairTest=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);JC(e,a,c,d)}; +Z.prototype.contactTest=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);KC(d,a,c)};Z.prototype.updateSingleAabb=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);LC(c,a)};Z.prototype.setDebugDrawer=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);MC(c,a)};Z.prototype.getDebugDrawer=function(){return k(NC(this.kB),WD)};Z.prototype.debugDrawWorld=function(){OC(this.kB)}; +Z.prototype.debugDrawObject=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);PC(e,a,c,d)};Z.prototype.setGravity=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);QC(c,a)};Z.prototype.getGravity=function(){return k(RC(this.kB),m)}; +Z.prototype.addRigidBody=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?SC(e,a):void 0===d?_emscripten_bind_btSoftRigidDynamicsWorld_addRigidBody_2(e,a,c):TC(e,a,c,d)};Z.prototype.removeRigidBody=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);UC(c,a)}; +Z.prototype.addConstraint=function(a,c){var d=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);void 0===c?VC(d,a):WC(d,a,c)};Z.prototype.removeConstraint=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);XC(c,a)};Z.prototype.stepSimulation=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);return void 0===c?YC(e,a):void 0===d?ZC(e,a,c):$C(e,a,c,d)}; +Z.prototype.setContactAddedCallback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);aD(c,a)};Z.prototype.setContactProcessedCallback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);bD(c,a)};Z.prototype.setContactDestroyedCallback=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);cD(c,a)};Z.prototype.addAction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);dD(c,a)};Z.prototype.removeAction=function(a){var c=this.kB;a&&"object"===typeof a&&(a=a.kB);eD(c,a)}; +Z.prototype.getSolverInfo=function(){return k(fD(this.kB),t)};Z.prototype.setInternalTickCallback=function(a,c,d){var e=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);void 0===c?gD(e,a):void 0===d?hD(e,a,c):iD(e,a,c,d)};Z.prototype.__destroy__=function(){jD(this.kB)};function IF(){this.kB=kD();h(IF)[this.kB]=this}IF.prototype=Object.create(f.prototype);IF.prototype.constructor=IF;IF.prototype.lB=IF;IF.mB={};b.btSoftBodyHelpers=IF; +IF.prototype.CreateRope=function(a,c,d,e,g){var n=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);return k(lD(n,a,c,d,e,g),Y)}; +IF.prototype.CreatePatch=function(a,c,d,e,g,n,z,T,Da){var dc=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);n&&"object"===typeof n&&(n=n.kB);z&&"object"===typeof z&&(z=z.kB);T&&"object"===typeof T&&(T=T.kB);Da&&"object"===typeof Da&&(Da=Da.kB);return k(mD(dc,a,c,d,e,g,n,z,T,Da),Y)}; +IF.prototype.CreatePatchUV=function(a,c,d,e,g,n,z,T,Da,dc){var JF=this.kB;ND();a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);n&&"object"===typeof n&&(n=n.kB);z&&"object"===typeof z&&(z=z.kB);T&&"object"===typeof T&&(T=T.kB);Da&&"object"===typeof Da&&(Da=Da.kB);"object"==typeof dc&&(dc=RD(dc));return k(nD(JF,a,c,d,e,g,n,z,T,Da,dc),Y)}; +IF.prototype.CreateEllipsoid=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);return k(oD(g,a,c,d,e),Y)}; +IF.prototype.CreateFromTriMesh=function(a,c,d,e,g){var n=this.kB;ND();a&&"object"===typeof a&&(a=a.kB);"object"==typeof c&&(c=RD(c));if("object"==typeof d&&"object"===typeof d){var z=OD(d,va);PD(d,va,z);d=z}e&&"object"===typeof e&&(e=e.kB);g&&"object"===typeof g&&(g=g.kB);return k(pD(n,a,c,d,e,g),Y)}; +IF.prototype.CreateFromConvexHull=function(a,c,d,e){var g=this.kB;a&&"object"===typeof a&&(a=a.kB);c&&"object"===typeof c&&(c=c.kB);d&&"object"===typeof d&&(d=d.kB);e&&"object"===typeof e&&(e=e.kB);return k(qD(g,a,c,d,e),Y)};IF.prototype.__destroy__=function(){rD(this.kB)}; +(function(){function a(){b.PHY_FLOAT=sD();b.PHY_DOUBLE=tD();b.PHY_INTEGER=uD();b.PHY_SHORT=vD();b.PHY_FIXEDPOINT88=wD();b.PHY_UCHAR=xD();b.CONST_GIMPACT_COMPOUND_SHAPE=yD();b.CONST_GIMPACT_TRIMESH_SHAPE_PART=zD();b.CONST_GIMPACT_TRIMESH_SHAPE=AD();b.BT_CONSTRAINT_ERP=BD();b.BT_CONSTRAINT_STOP_ERP=CD();b.BT_CONSTRAINT_CFM=DD();b.BT_CONSTRAINT_STOP_CFM=ED()}Ca?a():Aa.unshift(a)})();b.CONTACT_ADDED_CALLBACK_SIGNATURE="iiiiiiii";b.CONTACT_DESTROYED_CALLBACK_SIGNATURE="ii"; +b.CONTACT_PROCESSED_CALLBACK_SIGNATURE="iiii";b.INTERNAL_TICK_CALLBACK_SIGNATURE="vif";this.Ammo=b; + + + return Ammo.ready +} + +); +})(); +if (typeof exports === 'object' && typeof module === 'object') + module.exports = Ammo; +else if (typeof define === 'function' && define['amd']) + define([], function() { return Ammo; }); +else if (typeof exports === 'object') + exports["Ammo"] = Ammo; diff --git a/apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/ammo.wasm.wasm b/apps/control-station/public/vendor/playcanvas/ammo/2026-08-25/ammo.wasm.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a013d060a55a1d67dd3985844880b0c74b5223e9 GIT binary patch literal 747690 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

Hi`8njmHhBfR%Qi-A7Ppad?6^dCT&g=R(;fHM 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 literal 0 HcmV?d00001 diff --git a/apps/control-station/src/components/simulation/PlayCanvasRuntime.ts b/apps/control-station/src/components/simulation/PlayCanvasRuntime.ts index e18c7ac..327fe72 100644 --- a/apps/control-station/src/components/simulation/PlayCanvasRuntime.ts +++ b/apps/control-station/src/components/simulation/PlayCanvasRuntime.ts @@ -1,25 +1,34 @@ import { Application, Asset, + BLEND_NONE, BLEND_NORMAL, Color, CULLFACE_NONE, Entity, FILLMODE_NONE, Mat4, + Quat, RESOLUTION_AUTO, StandardMaterial, Vec2, Vec3, WasmModule, type ContainerResource, - type RenderComponent, + type ModelComponent, type ScriptType, } from "playcanvas"; import { CameraControls } from "playcanvas/scripts/esm/camera-controls.mjs"; +import { + prepareSimulationPhysics, + SimulationUgvController, + type AmmoApi, +} from "./SimulationUgvController"; + import type { - SimulationTransformAxis, + SimulationEulerRotation, + SimulationUgvPreset, SimulationViewerQuality, SimulationWorldManifest, } from "../../core/simulation/projects"; @@ -28,46 +37,18 @@ export type SimulationViewMode = "visual" | "collision" | "combined"; export type SimulationQuality = SimulationViewerQuality; export type SimulationLayer = "visual" | "collision"; export type SimulationCameraAxis = "horizontal" | "vertical"; +export type SimulationControlMode = "free" | "ugv"; -export const PLAYCANVAS_IDENTITY_TRANSFORM = [ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, -]; - -export const PLAYCANVAS_X_180_TRANSFORM = [ - 1, 0, 0, 0, - 0, -1, 0, 0, - 0, 0, -1, 0, - 0, 0, 0, 1, -]; - -export const PLAYCANVAS_Y_180_TRANSFORM = [ - -1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, -1, 0, - 0, 0, 0, 1, -]; - -export const PLAYCANVAS_Z_180_TRANSFORM = [ - -1, 0, 0, 0, - 0, -1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, -]; - -export function playCanvasAxisTransform(axis: SimulationTransformAxis, inverted: boolean): number[] { - if (!inverted) return PLAYCANVAS_IDENTITY_TRANSFORM; - if (axis === "x") return PLAYCANVAS_X_180_TRANSFORM; - if (axis === "y") return PLAYCANVAS_Y_180_TRANSFORM; - return PLAYCANVAS_Z_180_TRANSFORM; +export function playCanvasEulerTransform(rotation: SimulationEulerRotation): number[] { + return Array.from(new Mat4().setFromEulerAngles(rotation.x, rotation.y, rotation.z).data); } export interface SimulationRuntime { mount(canvas: HTMLCanvasElement): Promise; loadWorld(manifest: SimulationWorldManifest): Promise; setViewMode(mode: SimulationViewMode): Promise; + setControlMode(mode: SimulationControlMode): Promise; + setUgvPreset(preset: SimulationUgvPreset): void; setQuality(quality: SimulationQuality): void; setLayerWorldTransform(layer: SimulationLayer, transform: number[]): void; setCameraInversion(axis: SimulationCameraAxis, inverted: boolean): void; @@ -78,10 +59,6 @@ export interface SimulationRuntime { type DesktopCameraInput = { read(): { mouse: number[] } & Record; - _pointerId?: number; - _button?: number[]; - _keyNow?: number[]; - _keyPrev?: number[]; }; type CameraController = ScriptType & Pick & { @@ -123,6 +100,7 @@ export class PlayCanvasRuntime implements SimulationRuntime { private visualEntity: Entity | null = null; private visualAsset: Asset | null = null; private collisionEntity: Entity | null = null; + private collisionVisuals: ModelComponent[] = []; private collisionAsset: Asset | null = null; private collisionMaterial: StandardMaterial | null = null; private collisionSource: { meshUrl: string; projectId: string; worldTransform: number[] } | null = null; @@ -131,23 +109,41 @@ export class PlayCanvasRuntime implements SimulationRuntime { private viewMode: SimulationViewMode = "visual"; private quality: SimulationQuality = "high"; private cameraInversion = { horizontal: true, vertical: false }; + private controlMode: SimulationControlMode = "free"; + private controlRequestVersion = 0; + private ugvController: SimulationUgvController | null = null; + private readonly lastCameraPosition = new Vec3(); + private readonly lastCameraRotation = new Quat(); + private cameraSnapshotReady = false; private renderIntervalMs = 1000 / 60; private renderAccumulatorMs = 0; private disposed = false; - private scheduleFrame = (deltaMs: number): void => { - if (!this.app || this.disposed) return; - this.renderAccumulatorMs += deltaMs; + private updateFrame = (deltaSeconds: number): void => { + if (!this.app || !this.camera || this.disposed) return; + this.ugvController?.update(deltaSeconds); + const position = this.camera.getPosition(); + const rotation = this.camera.getRotation(); + const cameraChanged = !this.cameraSnapshotReady + || !position.equals(this.lastCameraPosition) + || !rotation.equals(this.lastCameraRotation); + if (!cameraChanged && this.controlMode === "free") return; + this.lastCameraPosition.copy(position); + this.lastCameraRotation.copy(rotation); + this.cameraSnapshotReady = true; + this.renderAccumulatorMs += deltaSeconds * 1000; if (this.renderAccumulatorMs < this.renderIntervalMs) return; this.renderAccumulatorMs %= this.renderIntervalMs; this.app.renderNextFrame = true; }; async mount(canvas: HTMLCanvasElement): Promise { - if (this.app) throw new Error("PlayCanvas runtime уже смонтирован."); + if (this.app) throw new Error("Runtime сцены уже запущен."); this.disposed = false; this.renderAccumulatorMs = 0; this.canvas = canvas; configureDracoDecoder(); + const ammo: AmmoApi = await prepareSimulationPhysics(); + if (this.disposed) return; canvas.tabIndex = 0; canvas.addEventListener("contextmenu", preventContextMenu); canvas.addEventListener("pointerdown", focusCanvas, true); @@ -161,7 +157,7 @@ export class PlayCanvasRuntime implements SimulationRuntime { }); this.app = app; app.autoRender = false; - app.on("frameupdate", this.scheduleFrame); + app.on("update", this.updateFrame); app.setCanvasFillMode(FILLMODE_NONE, 1, 1); app.setCanvasResolution(RESOLUTION_AUTO); app.scene.ambientLight = new Color(0.35, 0.37, 0.42); @@ -188,6 +184,7 @@ export class PlayCanvasRuntime implements SimulationRuntime { app.root.addChild(camera); this.camera = camera; this.cameraController = controller; + this.ugvController = new SimulationUgvController(app, canvas, camera, ammo); const light = new Entity("SimulationSun"); light.addComponent("light", { @@ -210,7 +207,7 @@ export class PlayCanvasRuntime implements SimulationRuntime { const app = this.requiredApp(); this.unloadWorld(); const sourceUrl = manifest.visual.streamedSogUrl ?? manifest.visual.previewSogUrl; - if (!sourceUrl) throw new Error("World manifest не содержит визуальный SOG."); + if (!sourceUrl) throw new Error("Манифест локации не содержит визуальный слой."); const asset = new Asset(`SimulationWorld:${manifest.projectId}`, "gsplat", { url: sourceUrl }); app.assets.add(asset); this.visualAsset = asset; @@ -233,10 +230,11 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.visualEntity = visual; this.applyQuality(); this.applyViewMode(); + this.requestRender(); resolve(); }; - const failed = (error: unknown) => { - reject(new Error(error instanceof Error ? error.message : "PlayCanvas не загрузил SOG.")); + const failed = (_error: unknown) => { + reject(new Error("Визуальный слой не загрузился.")); }; asset.ready(ready); asset.once("error", failed); @@ -254,7 +252,6 @@ export class PlayCanvasRuntime implements SimulationRuntime { } async setViewMode(mode: SimulationViewMode): Promise { - this.resetCameraInteraction(); this.viewMode = mode; this.applyViewMode(); try { @@ -263,13 +260,58 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.applyViewMode(); } } finally { + this.requestRender(); this.canvas?.focus({ preventScroll: true }); } } + async setControlMode(mode: SimulationControlMode): Promise { + const requestVersion = ++this.controlRequestVersion; + const controller = this.ugvController; + if (!controller) throw new Error("Управление UGV ещё не готово."); + + if (mode === "free") { + controller.disable(); + this.controlMode = "free"; + if (this.cameraController) this.cameraController.enabled = true; + this.home(); + return; + } + if (this.controlMode === "ugv" && controller.enabled) return; + + try { + await this.ensureCollision(); + if (requestVersion !== this.controlRequestVersion || this.disposed) return; + const collisionWorld = this.collisionEntity; + if (!collisionWorld) throw new Error("Физический слой UGV отсутствует."); + if (this.cameraController) this.cameraController.enabled = false; + await controller.enable(collisionWorld); + if (requestVersion !== this.controlRequestVersion || this.disposed) { + controller.disable(); + if (this.cameraController) this.cameraController.enabled = true; + return; + } + this.controlMode = "ugv"; + this.cameraSnapshotReady = false; + this.requestRender(); + } catch (error) { + controller.disable(); + this.controlMode = "free"; + if (this.cameraController) this.cameraController.enabled = true; + throw error; + } + } + setQuality(quality: SimulationQuality): void { this.quality = quality; this.applyQuality(); + this.requestRender(); + } + + setUgvPreset(preset: SimulationUgvPreset): void { + this.ugvController?.configure(preset); + this.cameraSnapshotReady = false; + this.requestRender(); } setLayerWorldTransform(layer: SimulationLayer, transform: number[]): void { @@ -278,10 +320,13 @@ export class PlayCanvasRuntime implements SimulationRuntime { } if (layer === "visual") { if (this.visualEntity) applyWorldTransform(this.visualEntity, transform); + this.requestRender(); return; } if (this.collisionSource) this.collisionSource.worldTransform = [...transform]; if (this.collisionEntity) applyWorldTransform(this.collisionEntity, transform); + this.ugvController?.syncCollisionBodies(); + this.requestRender(); } setCameraInversion(axis: SimulationCameraAxis, inverted: boolean): void { @@ -289,6 +334,12 @@ export class PlayCanvasRuntime implements SimulationRuntime { } home(): void { + if (this.controlMode === "ugv" && this.ugvController?.enabled) { + this.ugvController.reset(); + this.cameraSnapshotReady = false; + this.requestRender(); + return; + } this.resetCameraInteraction(); if (this.cameraController) { this.cameraController.reset(HOME_FOCUS, HOME_POSITION); @@ -296,6 +347,7 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.camera.setPosition(HOME_POSITION); this.camera.lookAt(HOME_FOCUS); } + this.requestRender(); this.canvas?.focus({ preventScroll: true }); } @@ -312,6 +364,7 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.camera.setPosition(position); this.camera.lookAt(focus); } + this.requestRender(); } dispose(): void { @@ -321,10 +374,12 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.resizeObserver = null; const webgl = this.canvas?.getContext("webgl2") ?? this.canvas?.getContext("webgl"); this.unloadWorld(); + this.ugvController?.dispose(); + this.ugvController = null; if (this.app) { this.app.autoRender = false; this.app.renderNextFrame = false; - this.app.off("frameupdate", this.scheduleFrame); + this.app.off("update", this.updateFrame); this.app.destroy(); } webgl?.getExtension("WEBGL_lose_context")?.loseContext(); @@ -340,19 +395,26 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.canvas = null; this.camera = null; this.cameraController = null; + this.controlMode = "free"; + this.cameraSnapshotReady = false; } private requiredApp(): Application { - if (!this.app) throw new Error("PlayCanvas runtime не смонтирован."); + if (!this.app) throw new Error("Runtime сцены не запущен."); return this.app; } + private requestRender(): void { + if (this.app && !this.disposed) this.app.renderNextFrame = true; + } + private resize(): void { if (!this.app || !this.canvas) return; const bounds = this.canvas.parentElement?.getBoundingClientRect(); const width = Math.max(1, Math.round(bounds?.width ?? this.canvas.clientWidth)); const height = Math.max(1, Math.round(bounds?.height ?? this.canvas.clientHeight)); this.app.resizeCanvas(width, height); + this.requestRender(); } private applyViewMode(): void { @@ -360,36 +422,27 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.visualEntity.enabled = this.viewMode !== "collision" || !this.collisionEntity; } if (this.collisionEntity) { - this.collisionEntity.enabled = this.viewMode !== "visual"; + // Physics bodies must stay active while their green debug geometry is hidden. + this.collisionEntity.enabled = true; + const visible = this.viewMode !== "visual"; + for (const model of this.collisionVisuals) model.enabled = visible; } if (this.collisionMaterial) { const combined = this.viewMode === "combined"; - this.collisionMaterial.opacity = combined ? 0.34 : 0.82; + this.collisionMaterial.opacity = combined ? 0.34 : 1; + this.collisionMaterial.blendType = combined ? BLEND_NORMAL : BLEND_NONE; this.collisionMaterial.depthWrite = !combined; this.collisionMaterial.update(); } + this.requestRender(); } private resetCameraInteraction(): void { const controller = this.cameraController; - const canvas = this.canvas; - if (!controller || !canvas) return; - const input = controller._desktopInput; - const pointerId = input?._pointerId ?? -1; - if (pointerId >= 0 && canvas.hasPointerCapture(pointerId)) { - try { - canvas.releasePointerCapture(pointerId); - } catch { - // The browser may already have released capture after a trackpad gesture. - } - } - if (input) { - input._pointerId = -1; - input._button?.fill(0); - input._keyNow?.fill(0); - input._keyPrev?.fill(0); - input.read(); - } + if (!controller) return; + // CameraControls owns pointer capture and its input-source lifecycle. Mutating + // those private fields here left the source unable to claim the next drag + // after a layer switch. The documented state event drains pending deltas. controller.fire("state"); const state = controller._state; if (state) { @@ -430,45 +483,52 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.collisionAsset = asset; await new Promise((resolve, reject) => { asset.ready((loaded) => { - if (this.disposed || !this.app) { - loaded.unload(); - resolve(); - return; - } - const resource = loaded.resource as ContainerResource | null; - if (!resource) { - reject(new Error("PlayCanvas не открыл collision GLB.")); - return; - } - const entity = resource.instantiateRenderEntity({ - castShadows: false, - receiveShadows: false, - }); - entity.name = "CollisionWorld"; - entity.enabled = false; - applyWorldTransform(entity, worldTransform); - app.root.addChild(entity); - - const material = new StandardMaterial(); - material.name = "SimulationCollisionMaterial"; - material.diffuse = new Color(0.2, 0.95, 0.42); - material.emissive = new Color(0.08, 0.36, 0.14); - material.opacity = 0.34; - material.blendType = BLEND_NORMAL; - material.depthWrite = false; - material.cull = CULLFACE_NONE; - material.update(); - for (const render of entity.findComponents("render") as RenderComponent[]) { - for (const meshInstance of render.meshInstances) { - meshInstance.material = material; + try { + if (this.disposed || !this.app) { + loaded.unload(); + resolve(); + return; } + const resource = loaded.resource as ContainerResource | null; + if (!resource) throw new Error("Контейнер GLB не содержит ресурса."); + const entity = resource.instantiateModelEntity({ + castShadows: false, + receiveShadows: false, + }); + entity.name = "CollisionWorld"; + entity.enabled = false; + applyWorldTransform(entity, worldTransform); + app.root.addChild(entity); + + const material = new StandardMaterial(); + material.name = "SimulationCollisionMaterial"; + material.diffuse = new Color(0.2, 0.95, 0.42); + material.emissive = new Color(0.08, 0.36, 0.14); + material.opacity = 0.34; + material.blendType = BLEND_NORMAL; + material.depthWrite = false; + material.cull = CULLFACE_NONE; + material.update(); + const models = entity.findComponents("model") as ModelComponent[]; + for (const model of models) { + for (const meshInstance of model.meshInstances ?? []) { + meshInstance.material = material; + } + } + this.collisionEntity = entity; + this.collisionVisuals = models; + this.collisionMaterial = material; + this.applyViewMode(); + this.requestRender(); + resolve(); + } catch (error) { + reject(new Error( + `Слой коллизий не открылся: ${error instanceof Error ? error.message : String(error)}`, + )); } - this.collisionEntity = entity; - this.collisionMaterial = material; - resolve(); }); asset.once("error", (error: unknown) => { - reject(new Error(error instanceof Error ? error.message : "PlayCanvas не загрузил collision GLB.")); + reject(new Error(`Слой коллизий не загрузился: ${String(error)}`)); }); app.assets.load(asset); }); @@ -477,7 +537,7 @@ export class PlayCanvasRuntime implements SimulationRuntime { private async ensureCollision(): Promise { if (this.collisionEntity) return; if (!this.collisionSource) { - throw new Error("Collision GLB отсутствует в world manifest."); + throw new Error("Слой коллизий отсутствует в манифесте локации."); } if (!this.collisionLoadPromise) { this.collisionLoadPromise = this.loadCollision( @@ -492,6 +552,10 @@ export class PlayCanvasRuntime implements SimulationRuntime { } private unloadWorld(): void { + this.controlRequestVersion += 1; + this.ugvController?.disable(); + this.controlMode = "free"; + if (this.cameraController) this.cameraController.enabled = true; if (this.visualEntity) { this.visualEntity.destroy(); this.visualEntity = null; @@ -505,6 +569,7 @@ export class PlayCanvasRuntime implements SimulationRuntime { this.collisionEntity.destroy(); this.collisionEntity = null; } + this.collisionVisuals = []; if (this.collisionAsset && this.app) { this.app.assets.remove(this.collisionAsset); this.collisionAsset.unload(); diff --git a/apps/control-station/src/components/simulation/SimulationCatalog.tsx b/apps/control-station/src/components/simulation/SimulationCatalog.tsx new file mode 100644 index 0000000..2c792f0 --- /dev/null +++ b/apps/control-station/src/components/simulation/SimulationCatalog.tsx @@ -0,0 +1,333 @@ +import { + ActivityIndicator, + Button, + Icon, + IconButton, + StatusBadge, +} from "@nodedc/ui-react"; + +import type { + SimulationProject, + SimulationProjectStatus, + SimulationUploadProgress, +} from "../../core/simulation/projects"; +import { estimateSimulationProcessing } from "../../core/simulation/projects"; + +const ACTIVE_STATUSES = new Set([ + "uploading", + "queued", + "processing", + "importing", +]); + +export function SimulationCatalog({ + projects, + uploadProgress, + loading, + loadError, + queueError, + dropping, + onCreate, + onRetryLoad, + onSelect, + onEdit, + onDelete, +}: { + projects: SimulationProject[]; + uploadProgress: ReadonlyMap; + loading: boolean; + loadError: string | null; + queueError: string | null; + dropping: boolean; + onCreate: () => void; + onRetryLoad: () => void; + onSelect: (project: SimulationProject) => void; + onEdit: (project: SimulationProject) => void; + onDelete: (project: SimulationProject) => void; +}) { + const sourceBytes = projects.reduce( + (sum, project) => sum + project.source.totalByteLength, + 0, + ); + + return ( +

+
+
Проектов{projects.length.toLocaleString("ru-RU")}
+
Визуал готов{countStatus(projects, "ready")}
+
В работе{projects.filter((project) => ACTIVE_STATUSES.has(project.status)).length}
+
Исходники{formatBytes(sourceBytes)}
+
+ + {queueError ? ( +
+ + {queueError} +
+ ) : null} + + {loading && projects.length === 0 ? ( +
+ + Читаем каталог сцен +
+ ) : loadError && projects.length === 0 ? ( +
+ +
Каталог недоступен{loadError}
+ +
+ ) : projects.length === 0 ? ( +
+ + Перетащите архивы в это окно +

Каждый ZIP, RAR или 7z сразу станет отдельным проектом и встанет в очередь.

+ +
+ ) : ( +
+ + + + + + + + + + + + + {projects.map((project) => ( + onSelect(project)} + onEdit={() => onEdit(project)} + onDelete={() => onDelete(project)} + /> + ))} + +
ПроектИсточникРазмерСплатыСтатусОбновлён +
+
+ )} + + {dropping ? ( +
+ + Отпустите архивы + ZIP · RAR · 7z · до 16 ГБ на проект + Внутри — сцена LCC или LCC2 и связанные файлы +
+ ) : null} +
+ ); +} + +function SimulationCatalogRow({ + project, + uploadProgress, + onSelect, + onEdit, + onDelete, +}: { + project: SimulationProject; + uploadProgress?: SimulationUploadProgress; + onSelect: () => void; + onEdit: () => void; + onDelete: () => void; +}) { + const status = statusPresentation(project.status); + const active = ACTIVE_STATUSES.has(project.status); + return ( + + + + + {active ? ( + + + + ) : ( + <> + + {project.source.kind === "archive" ? "Архив" : "Папка"} + {project.source.files.length} файл(ов) + + {formatBytes(project.source.totalByteLength)} + — + + )} + {status.label} + {formatDate(project.updatedAtUtc)} + +
+ + + + + + +
+ + + ); +} + +function ProjectProgress({ + project, + localProgress, +}: { + project: SimulationProject; + localProgress?: SimulationUploadProgress; +}) { + const progress = progressPresentation(project, localProgress); + return ( +
+ {progress.meta ? {progress.meta} : null} + + + + {progress.label} +
+ ); +} + +function progressPresentation( + project: SimulationProject, + local?: SimulationUploadProgress, +): { percent: number; label: string; meta: string | null; indeterminate: boolean } { + if (project.status === "uploading") { + const uploaded = local?.uploadedBytes ?? project.source.uploadedByteLength; + const total = local?.totalBytes ?? project.source.totalByteLength; + const percent = total ? Math.min(100, uploaded / total * 100) : 0; + return { + percent, + label: `${formatBytes(uploaded)} / ${formatBytes(total)}`, + meta: progressMeta(percent, local?.estimatedSecondsRemaining), + indeterminate: false, + }; + } + if (project.status === "queued") { + return { percent: 0, label: "Ожидает обработки", meta: null, indeterminate: true }; + } + const estimate = estimateSimulationProcessing(project); + if (estimate) { + return { + percent: estimate.percent, + label: processingLabel(project), + meta: progressMeta(estimate.percent, estimate.estimatedSecondsRemaining), + indeterminate: false, + }; + } + const completed = project.provider.progress?.completed_steps; + const total = project.provider.progress?.total_steps; + if (typeof completed === "number" && typeof total === "number" && total > 0) { + const percent = Math.min(100, completed / total * 100); + return { + percent, + label: processingLabel(project), + meta: progressMeta(percent, null), + indeterminate: false, + }; + } + return { + percent: project.status === "importing" ? 96 : 0, + label: processingLabel(project), + meta: project.status === "importing" ? progressMeta(96, null) : null, + indeterminate: project.status !== "importing", + }; +} + +function progressMeta(percent: number, seconds: number | null | undefined): string { + const roundedPercent = Math.min(100, Math.max(0, Math.floor(percent))); + return `Прогресс ${roundedPercent}% | ${remainingTimeLabel(seconds)}`; +} + +function remainingTimeLabel(seconds: number | null | undefined): string { + if (seconds === null || seconds === undefined) return "Время уточняется"; + if (seconds <= 60) return "Осталось меньше минуты"; + const minutes = Math.ceil(seconds / 60); + if (minutes < 60) return `Осталось примерно ${minutes} мин`; + const hours = Math.floor(minutes / 60); + const remainder = minutes % 60; + return `Осталось примерно ${hours} ч${remainder ? ` ${remainder} мин` : ""}`; +} + +function processingLabel(project: SimulationProject): string { + if (project.status === "importing") return "Подготавливаем сцену"; + const stage = project.provider.state; + if (stage === "verifying_source") return "Проверяем исходник"; + if (stage === "inspecting") return "Анализируем локацию"; + if (stage === "building_preview") return "Подготавливаем локацию"; + if (stage === "building_streamed_sog") return "Собираем уровни детализации"; + if (stage === "building_collision") return "Собираем коллизии"; + return "Обрабатываем локацию"; +} + +function statusPresentation(status: SimulationProjectStatus): { + label: string; + tone: "success" | "accent" | "warning" | "neutral"; +} { + if (status === "ready") return { label: "Визуал готов", tone: "success" }; + if (status === "failed") return { label: "Ошибка", tone: "warning" }; + if (status === "uploading") return { label: "Загрузка", tone: "neutral" }; + if (status === "queued") return { label: "В очереди", tone: "neutral" }; + if (status === "importing") return { label: "Подготовка", tone: "accent" }; + return { label: "Обработка", tone: "accent" }; +} + +function sourceFileLabel(project: SimulationProject): string { + const first = project.source.files[0]?.logicalPath; + if (project.source.kind === "archive" && first) return first; + return `${sceneTypeLabel(project.sceneType)} · ${project.source.files.length} файл(ов)`; +} + +function sceneTypeLabel(value: SimulationProject["sceneType"]): string { + if (value === "interior") return "Интерьер"; + if (value === "object") return "Объект"; + return "Улица"; +} + +function countStatus(projects: SimulationProject[], status: SimulationProjectStatus): number { + return projects.filter((project) => project.status === status).length; +} + +function formatDate(value: string): string { + const date = new Date(value); + return Number.isNaN(date.valueOf()) ? "—" : date.toLocaleString("ru-RU", { + day: "2-digit", + month: "short", + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatBytes(value: number): string { + if (value < 1024) return `${value} Б`; + if (value < 1024 ** 2) return `${(value / 1024).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} КБ`; + if (value < 1024 ** 3) return `${(value / 1024 ** 2).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} МБ`; + return `${(value / 1024 ** 3).toLocaleString("ru-RU", { maximumFractionDigits: 2 })} ГБ`; +} diff --git a/apps/control-station/src/components/simulation/SimulationProjectWindow.tsx b/apps/control-station/src/components/simulation/SimulationProjectWindow.tsx index 9dc0881..fcf532b 100644 --- a/apps/control-station/src/components/simulation/SimulationProjectWindow.tsx +++ b/apps/control-station/src/components/simulation/SimulationProjectWindow.tsx @@ -18,6 +18,10 @@ import { type SimulationUploadCandidate, type SimulationUploadProgress, } from "../../core/simulation/projects"; +import { + candidatesFromDrop, + validateSimulationCandidates, +} from "../../core/simulation/sourceFiles"; export function SimulationProjectWindow({ open, @@ -55,7 +59,7 @@ export function SimulationProjectWindow({ const acceptCandidates = (next: SimulationUploadCandidate[]) => { try { - validateCandidates(next); + validateSimulationCandidates(next); setCandidates(next); setError(null); if (!name.trim() && next.length) { @@ -213,7 +217,10 @@ export function SimulationProjectWindow({
- {formatBytes(progress.uploadedBytes)} / {formatBytes(progress.totalBytes)} + + {formatBytes(progress.uploadedBytes)} / {formatBytes(progress.totalBytes)} + {" · "}{formatUploadEta(progress.estimatedSecondsRemaining)} + {progress.currentPath}
@@ -224,83 +231,15 @@ export function SimulationProjectWindow({ ); } -interface LegacyFileEntry { - isFile: boolean; - isDirectory: boolean; - name: string; - fullPath: string; - file?: (callback: (file: File) => void, error?: (error: DOMException) => void) => void; - createReader?: () => { - readEntries: ( - callback: (entries: LegacyFileEntry[]) => void, - error?: (error: DOMException) => void, - ) => void; - }; -} - -async function candidatesFromDrop(transfer: DataTransfer): Promise { - const candidates: SimulationUploadCandidate[] = []; - for (const item of Array.from(transfer.items)) { - const entry = (item as DataTransferItem & { - webkitGetAsEntry?: () => LegacyFileEntry | null; - }).webkitGetAsEntry?.(); - if (entry) { - await collectEntry(entry, candidates); - continue; - } - const file = item.getAsFile(); - if (file) candidates.push({ file, logicalPath: file.name }); - } - if (!candidates.length) { - for (const file of Array.from(transfer.files)) { - candidates.push({ file, logicalPath: file.name }); - } - } - return candidates; -} - -async function collectEntry(entry: LegacyFileEntry, target: SimulationUploadCandidate[]): Promise { - if (entry.isFile && entry.file) { - const file = await new Promise((resolve, reject) => entry.file?.(resolve, reject)); - target.push({ file, logicalPath: entry.fullPath.replace(/^\//, "") || file.name }); - return; - } - if (!entry.isDirectory || !entry.createReader) return; - const reader = entry.createReader(); - while (true) { - const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject)); - if (!batch.length) break; - for (const child of batch) await collectEntry(child, target); - } -} - -function validateCandidates(candidates: SimulationUploadCandidate[]): void { - if (!candidates.length) throw new Error("Выберите архив или папку с результатом LCC/LCC2."); - const archives = candidates.filter((candidate) => /\.(zip|rar|7z)$/i.test(candidate.logicalPath)); - if (archives.length) { - if (candidates.length !== 1) throw new Error("Архив нужно загружать одним файлом."); - return; - } - const descriptors = candidates.filter((candidate) => /\.lcc2?$/i.test(candidate.logicalPath)); - if (descriptors.length !== 1) { - throw new Error("В папке должна быть ровно одна сцена .lcc или .lcc2."); - } - const paths = new Set(); - for (const candidate of candidates) { - const parts = candidate.logicalPath.split("/"); - if ( - !candidate.logicalPath || - candidate.logicalPath.startsWith("/") || - candidate.logicalPath.includes("\\") || - candidate.logicalPath.includes("\0") || - parts.some((part) => part === "" || part === "." || part === "..") - ) { - throw new Error("Папка содержит небезопасный путь."); - } - if (candidate.file.size <= 0) throw new Error(`Пустой файл: ${candidate.logicalPath}`); - if (paths.has(candidate.logicalPath)) throw new Error(`Повторяющийся путь: ${candidate.logicalPath}`); - paths.add(candidate.logicalPath); - } +function formatUploadEta(seconds: number | null): string { + if (seconds === 0) return "загружено"; + if (seconds === null) return "считаем время"; + if (seconds < 60) return `осталось ≈ ${Math.max(1, seconds)} сек`; + const minutes = Math.ceil(seconds / 60); + if (minutes < 60) return `осталось ≈ ${minutes} мин`; + const hours = Math.floor(minutes / 60); + const remainder = minutes % 60; + return `осталось ≈ ${hours} ч${remainder ? ` ${remainder} мин` : ""}`; } function summarizeCandidates(candidates: SimulationUploadCandidate[]): { title: string; detail: string } { diff --git a/apps/control-station/src/components/simulation/SimulationUgvController.ts b/apps/control-station/src/components/simulation/SimulationUgvController.ts new file mode 100644 index 0000000..d7b08e6 --- /dev/null +++ b/apps/control-station/src/components/simulation/SimulationUgvController.ts @@ -0,0 +1,999 @@ +import { + Application, + Color, + Entity, + Mat4, + Mesh, + PRIMITIVE_TRIANGLES, + Quat, + StandardMaterial, + Vec3, + WasmModule, + type ModelComponent, +} from "playcanvas"; +import { MeshoptSimplifier } from "meshoptimizer/simplifier"; +import type { SimulationUgvPreset } from "../../core/simulation/projects"; + +const AMMO_MODULE_ROOT = "/vendor/playcanvas/ammo/2026-08-25"; +const UGV_SPAWN_POSITION = new Vec3(0, 1.15, 0); +const UGV_SPAWN_ROTATION = new Vec3(0, 0, 0); +const ZERO = new Vec3(0, 0, 0); +const DISABLE_DEACTIVATION = 4; +const PHYSICS_PROXY_RATIO = 0.04; +const PHYSICS_PROXY_ERROR = 0.0003; +const PHYSICS_PROXY_MAX_TRIANGLES = 240_000; +const SERVICE_BRAKE_DECELERATION_MPS2 = 1.8; +const COAST_DECELERATION_MPS2 = 0.18; +const BRAKE_ATTITUDE_DAMPING = 6; +const DEFAULT_ORBIT_PITCH = 0.48; +const CAMERA_RETURN_DELAY_SECONDS = 1.2; +const CAMERA_RETURN_DURATION_SECONDS = 2; +const UGV_SPAWN_OFFSETS: ReadonlyArray = [ + [0, 0], + [1.5, 0], + [-1.5, 0], + [0, 1.5], + [0, -1.5], + [1.5, 1.5], + [-1.5, 1.5], + [1.5, -1.5], + [-1.5, -1.5], + [3, 0], + [-3, 0], + [0, 3], + [0, -3], +]; + +export const DEFAULT_SIMULATION_UGV_PRESET: SimulationUgvPreset = { + presetName: "UGV 100 кг", + massKg: 100, + dimensionsMeters: { + length: 1, + width: 0.8, + height: 0.4, + groundClearance: 0.15, + }, + maxSpeedMetersPerSecond: 1.2, + maxTurnRateDegrees: 45, + invertSteering: false, +}; + +type NativeObject = object; + +interface NativeVector3 extends NativeObject { + x(): number; + y(): number; + z(): number; + setValue(x: number, y: number, z: number): void; +} + +interface NativeQuaternion extends NativeObject { + x(): number; + y(): number; + z(): number; + w(): number; +} + +interface NativeTransform extends NativeObject { + getOrigin(): NativeVector3; + getRotation(): NativeQuaternion; +} + +interface NativeWheelInfo extends NativeObject { + set_m_suspensionStiffness(value: number): void; + set_m_wheelsDampingRelaxation(value: number): void; + set_m_wheelsDampingCompression(value: number): void; + set_m_frictionSlip(value: number): void; + set_m_rollInfluence(value: number): void; +} + +interface NativeRaycastVehicle extends NativeObject { + setCoordinateSystem(right: number, up: number, forward: number): void; + addWheel( + connectionPoint: NativeVector3, + direction: NativeVector3, + axle: NativeVector3, + suspensionRestLength: number, + radius: number, + tuning: NativeObject, + isFront: boolean, + ): NativeWheelInfo; + applyEngineForce(force: number, wheel: number): void; + setBrake(force: number, wheel: number): void; + setSteeringValue(value: number, wheel: number): void; + getNumWheels(): number; + updateWheelTransform(wheel: number, interpolated: boolean): void; + getWheelTransformWS(wheel: number): NativeTransform; + getForwardVector(): NativeVector3; + getCurrentSpeedKmHour(): number; + resetSuspension(): void; +} + +export interface AmmoApi { + btVehicleTuning: new () => NativeObject; + btDefaultVehicleRaycaster: new (world: NativeObject) => NativeObject; + btRaycastVehicle: new ( + tuning: NativeObject, + body: NativeObject, + raycaster: NativeObject, + ) => NativeRaycastVehicle; + btVector3: new (x: number, y: number, z: number) => NativeVector3; + destroy(object: NativeObject): void; +} + +interface NativeDynamicsWorld extends NativeObject { + addAction(action: NativeObject): void; + removeAction(action: NativeObject): void; +} + +interface PhysicsSystemAccess { + systems: { + rigidbody: { + dynamicsWorld: NativeDynamicsWorld | null; + raycastFirst(start: Vec3, end: Vec3): { point: Vec3; normal: Vec3 } | null; + }; + }; +} + +interface NativeRigidBodyAccess { + rigidbody?: { + body: NativeObject | null; + linearVelocity: Vec3; + angularVelocity: Vec3; + teleport(position: Vec3, rotation?: Vec3 | Quat): void; + }; +} + +interface WheelDefinition { + connection: Vec3; + left: boolean; + front: boolean; + anchor: Entity; +} + +let ammoInstancePromise: Promise | null = null; + +export function prepareSimulationPhysics(): Promise { + if (ammoInstancePromise) return ammoInstancePromise; + + ammoInstancePromise = new Promise((resolve, reject) => { + if (!WasmModule.getConfig("Ammo")) { + WasmModule.setConfig("Ammo", { + glueUrl: `${AMMO_MODULE_ROOT}/ammo.wasm.js`, + wasmUrl: `${AMMO_MODULE_ROOT}/ammo.wasm.wasm`, + errorHandler: (error) => { + ammoInstancePromise = null; + reject(new Error(`Физический модуль UGV не загрузился: ${String(error)}`)); + }, + }); + } + WasmModule.getInstance("Ammo", (instance) => { + if (!instance) { + ammoInstancePromise = null; + reject(new Error("Физический модуль UGV вернул пустой экземпляр.")); + return; + } + resolve(instance as AmmoApi); + }); + }); + + return ammoInstancePromise; +} + +export class SimulationUgvController { + private readonly pressed = new Set(); + private readonly collisionBodies: Entity[] = []; + private readonly collisionMeshes: Mesh[] = []; + private readonly wheelDefinitions: WheelDefinition[] = []; + private readonly cameraTarget = new Vec3(); + private readonly cameraDesired = new Vec3(); + private readonly cameraLookAt = new Vec3(); + private readonly forward = new Vec3(0, 0, 1); + private readonly smoothedCamera = new Vec3(); + private readonly limitedLinearVelocity = new Vec3(); + private readonly limitedAngularVelocity = new Vec3(); + private readonly spawnPosition = UGV_SPAWN_POSITION.clone(); + private orbitPointerId: number | null = null; + private orbitPointerX = 0; + private orbitPointerY = 0; + private orbitYaw = 0; + private orbitPitch = DEFAULT_ORBIT_PITCH; + private orbitDistance = 3.8; + private orbitIdleSeconds = 0; + private cameraOrbitInitialized = false; + private active = false; + private vehicleEntity: Entity | null = null; + private vehicle: NativeRaycastVehicle | null = null; + private vehicleTuning: NativeObject | null = null; + private vehicleRaycaster: NativeObject | null = null; + private dynamicsWorld: NativeDynamicsWorld | null = null; + private chassisMaterial: StandardMaterial | null = null; + private wheelMaterial: StandardMaterial | null = null; + private cameraInitialized = false; + private settings: SimulationUgvPreset; + + constructor( + private readonly app: Application, + private readonly canvas: HTMLCanvasElement, + private readonly camera: Entity, + private readonly ammo: AmmoApi, + initialSettings: SimulationUgvPreset = DEFAULT_SIMULATION_UGV_PRESET, + ) { + this.settings = cloneUgvPreset(initialSettings); + } + + get enabled(): boolean { + return this.active; + } + + configure(settings: SimulationUgvPreset): void { + const next = cloneUgvPreset(settings); + const rebuildRequired = this.active && vehicleGeometryChanged(this.settings, next); + this.settings = next; + if (!rebuildRequired) return; + this.destroyVehicle(); + this.spawnPosition.copy(this.findSafeSpawnPosition()); + this.createVehicle(); + this.reset(false, false); + } + + async enable(collisionWorld: Entity): Promise { + if (this.active) return; + await nextAnimationFrame(); + try { + await this.createStaticCollisionBodies(collisionWorld); + await nextAnimationFrame(); + this.spawnPosition.copy(this.findSafeSpawnPosition()); + this.createVehicle(); + this.attachInput(); + this.active = true; + this.reset(); + } catch (error) { + this.disable(); + throw error; + } + } + + disable(): void { + this.active = false; + this.detachInput(); + this.pressed.clear(); + + this.destroyVehicle(); + + for (const entity of this.collisionBodies.splice(0)) { + runCleanup("destroy static physics proxy", () => entity.destroy()); + } + for (const mesh of this.collisionMeshes.splice(0)) { + runCleanup("destroy static physics mesh", () => mesh.destroy()); + } + this.cameraInitialized = false; + this.cameraOrbitInitialized = false; + this.orbitIdleSeconds = 0; + } + + dispose(): void { + this.disable(); + } + + reset(focusCanvas = true, resetCameraOrbit = true): void { + const vehicleEntity = this.vehicleEntity as (Entity & NativeRigidBodyAccess) | null; + const rigidbody = vehicleEntity?.rigidbody; + if (!vehicleEntity || !rigidbody) return; + rigidbody.teleport(this.spawnPosition, UGV_SPAWN_ROTATION); + rigidbody.linearVelocity = ZERO; + rigidbody.angularVelocity = ZERO; + this.vehicle?.resetSuspension(); + this.cameraInitialized = false; + if (resetCameraOrbit) { + this.cameraOrbitInitialized = false; + this.orbitIdleSeconds = 0; + } + this.updateCamera(1); + if (focusCanvas) this.canvas.focus({ preventScroll: true }); + } + + syncCollisionBodies(): void { + for (const entity of this.collisionBodies) { + const rigidbody = (entity as Entity & NativeRigidBodyAccess).rigidbody; + rigidbody?.teleport(entity.getPosition(), entity.getRotation()); + } + } + + update(deltaSeconds: number): void { + if (!this.active || !this.vehicle || !this.vehicleEntity) return; + const forwardInput = Number(this.isPressed("KeyW", "ArrowUp")) - Number(this.isPressed("KeyS", "ArrowDown")); + const requestedTurn = Number(this.isPressed("KeyA", "ArrowLeft")) - Number(this.isPressed("KeyD", "ArrowRight")); + const turnInput = this.settings.invertSteering ? -requestedTurn : requestedTurn; + const braking = this.pressed.has("Space"); + const rigidbody = (this.vehicleEntity as Entity & NativeRigidBodyAccess).rigidbody; + const speedMetersPerSecond = this.vehicle.getCurrentSpeedKmHour() / 3.6; + const maxSpeed = this.settings.maxSpeedMetersPerSecond; + const maxTurnRate = this.settings.maxTurnRateDegrees * Math.PI / 180; + const pureTurn = !braking && forwardInput === 0 && turnInput !== 0; + const desiredSpeed = forwardInput * maxSpeed; + const speedError = desiredSpeed - speedMetersPerSecond; + const speedResponseRange = Math.max(0.35, maxSpeed * 0.2); + const throttle = forwardInput === 0 + ? 0 + : clamp(Math.abs(speedError) / speedResponseRange, 0, 1); + const maximumAcceleration = clamp(1.4 + maxSpeed * 0.35, 1.8, 4.2); + const driveForcePerWheel = this.settings.massKg * maximumAcceleration * throttle / 4; + const pivotForcePerWheel = this.settings.massKg * 0.65; + const forwardCommand = braking ? 0 : forwardInput; + const turnCommand = braking ? 0 : turnInput; + const leftCommand = clamp(forwardCommand - turnCommand, -1, 1); + const rightCommand = clamp(forwardCommand + turnCommand, -1, 1); + const engineForce = pureTurn ? pivotForcePerWheel : driveForcePerWheel; + + for (let index = 0; index < this.wheelDefinitions.length; index += 1) { + const definition = this.wheelDefinitions[index]; + const command = definition.left ? leftCommand : rightCommand; + this.vehicle.setSteeringValue(0, index); + this.vehicle.applyEngineForce(command * engineForce, index); + this.vehicle.setBrake(0, index); + this.vehicle.updateWheelTransform(index, true); + const transform = this.vehicle.getWheelTransformWS(index); + const position = transform.getOrigin(); + const rotation = transform.getRotation(); + definition.anchor.setPosition(position.x(), position.y(), position.z()); + definition.anchor.setRotation(rotation.x(), rotation.y(), rotation.z(), rotation.w()); + } + + if (rigidbody) { + const linearVelocity = rigidbody.linearVelocity; + let nextLinearX = linearVelocity.x; + let nextLinearZ = linearVelocity.z; + let horizontalSpeed = Math.hypot(nextLinearX, nextLinearZ); + const coasting = !braking && forwardInput === 0 && turnInput === 0; + if ((braking || coasting) && horizontalSpeed > 0.001) { + const deceleration = braking + ? SERVICE_BRAKE_DECELERATION_MPS2 + : COAST_DECELERATION_MPS2; + const nextSpeed = Math.max(0, horizontalSpeed - deceleration * Math.max(0, deltaSeconds)); + const scale = nextSpeed / horizontalSpeed; + nextLinearX *= scale; + nextLinearZ *= scale; + horizontalSpeed = nextSpeed; + } + if (pureTurn && horizontalSpeed > 0.001) { + const pivotDamping = Math.exp(-Math.max(0, deltaSeconds) * 8); + nextLinearX *= pivotDamping; + nextLinearZ *= pivotDamping; + horizontalSpeed *= pivotDamping; + } + if (horizontalSpeed > maxSpeed * 1.05) { + const scale = maxSpeed / horizontalSpeed; + nextLinearX *= scale; + nextLinearZ *= scale; + } + if (nextLinearX !== linearVelocity.x || nextLinearZ !== linearVelocity.z) { + this.limitedLinearVelocity.set(nextLinearX, linearVelocity.y, nextLinearZ); + rigidbody.linearVelocity = this.limitedLinearVelocity; + } + const angularVelocity = rigidbody.angularVelocity; + const attitudeDamping = braking + ? Math.exp(-Math.max(0, deltaSeconds) * BRAKE_ATTITUDE_DAMPING) + : 1; + let nextAngularY = angularVelocity.y; + if (pureTurn) { + const desiredYawRate = -turnInput * maxTurnRate; + const turnAcceleration = Math.max(2, maxTurnRate * 4); + nextAngularY = approach( + angularVelocity.y, + desiredYawRate, + turnAcceleration * Math.max(0, deltaSeconds), + ); + } else if (Math.abs(angularVelocity.y) > maxTurnRate) { + nextAngularY = Math.sign(angularVelocity.y) * maxTurnRate; + } + if (attitudeDamping !== 1 || nextAngularY !== angularVelocity.y) { + this.limitedAngularVelocity.set( + angularVelocity.x * attitudeDamping, + nextAngularY, + angularVelocity.z * attitudeDamping, + ); + rigidbody.angularVelocity = this.limitedAngularVelocity; + } + } + + const body = (this.vehicleEntity as Entity & NativeRigidBodyAccess).rigidbody?.body as { + setActivationState?: (state: number) => void; + } | null; + body?.setActivationState?.(DISABLE_DEACTIVATION); + if (this.vehicleEntity.getPosition().y < -8) this.reset(); + this.updateCamera(deltaSeconds); + } + + private async createStaticCollisionBodies(collisionWorld: Entity): Promise { + const models = collisionWorld.findComponents("model") as ModelComponent[]; + if (models.length === 0) throw new Error("В слое коллизий нет геометрии для физики UGV."); + + const meshInstances = models.flatMap((model) => Array.from(model.meshInstances ?? [])); + const totalTriangles = meshInstances.reduce( + (total, meshInstance) => total + Math.floor((meshInstance.mesh.primitive[0]?.count ?? 0) / 3), + 0, + ); + if (totalTriangles === 0) throw new Error("В слое коллизий нет треугольников для физики UGV."); + const targetRatio = totalTriangles <= PHYSICS_PROXY_MAX_TRIANGLES + ? 1 + : Math.min(PHYSICS_PROXY_RATIO, PHYSICS_PROXY_MAX_TRIANGLES / totalTriangles); + await MeshoptSimplifier.ready; + await nextAnimationFrame(); + + const rootInverse = collisionWorld.getWorldTransform().clone().invert(); + for (let index = 0; index < meshInstances.length; index += 1) { + const meshInstance = meshInstances[index]; + const physicsMesh = createPhysicsProxyMesh( + this.app, + meshInstance.mesh, + new Mat4().mul2(rootInverse, meshInstance.node.getWorldTransform()), + targetRatio, + ); + if (!physicsMesh) continue; + const entity = new Entity(`UGV physics proxy ${index + 1}`); + collisionWorld.addChild(entity); + entity.addComponent("collision", { + type: "mesh", + render: { meshes: [physicsMesh] }, + checkVertexDuplicates: false, + }); + entity.addComponent("rigidbody", { + type: "static", + friction: 0.95, + restitution: 0, + }); + this.collisionBodies.push(entity); + this.collisionMeshes.push(physicsMesh); + if ((index + 1) % 4 === 0) await nextAnimationFrame(); + } + + if (this.collisionBodies.length === 0) { + throw new Error("Не удалось создать статический физический слой UGV."); + } + } + + private createVehicle(): void { + const access = this.app as unknown as PhysicsSystemAccess; + const dynamicsWorld = access.systems.rigidbody.dynamicsWorld; + if (!dynamicsWorld) throw new Error("Физический мир PlayCanvas не готов."); + + const vehicle = new Entity("SimulationUGV"); + vehicle.addComponent("collision", { type: "compound" }); + vehicle.addComponent("rigidbody", { + type: "dynamic", + mass: this.settings.massKg, + friction: 0.85, + linearDamping: 0.08, + angularDamping: 0.45, + }); + + this.chassisMaterial = createMaterial(readThemeAccent(), new Color(0.03, 0.04, 0.05)); + this.wheelMaterial = createMaterial(new Color(0.055, 0.06, 0.07), new Color(0.008, 0.009, 0.01)); + + const dimensions = vehicleDimensions(this.settings); + const chassis = createBox( + "UGV chassis", + new Vec3(dimensions.bodyWidth, dimensions.mainHeight, dimensions.length), + this.chassisMaterial, + ); + chassis.setLocalPosition(0, dimensions.mainCenterY, 0); + chassis.addComponent("collision", { + type: "box", + halfExtents: new Vec3( + dimensions.bodyWidth / 2, + dimensions.mainHeight / 2, + dimensions.length / 2, + ), + }); + vehicle.addChild(chassis); + + const equipment = createBox( + "UGV equipment", + new Vec3(dimensions.bodyWidth * 0.68, dimensions.equipmentHeight, dimensions.length * 0.56), + this.chassisMaterial, + ); + equipment.setLocalPosition(0, dimensions.equipmentCenterY, -dimensions.length * 0.04); + equipment.addComponent("collision", { + type: "box", + halfExtents: new Vec3( + dimensions.bodyWidth * 0.34, + dimensions.equipmentHeight / 2, + dimensions.length * 0.28, + ), + }); + vehicle.addChild(equipment); + + const wheelConnections = [ + { connection: new Vec3(dimensions.wheelX, dimensions.wheelConnectionY, dimensions.wheelZ), left: false, front: true }, + { connection: new Vec3(-dimensions.wheelX, dimensions.wheelConnectionY, dimensions.wheelZ), left: true, front: true }, + { connection: new Vec3(dimensions.wheelX, dimensions.wheelConnectionY, -dimensions.wheelZ), left: false, front: false }, + { connection: new Vec3(-dimensions.wheelX, dimensions.wheelConnectionY, -dimensions.wheelZ), left: true, front: false }, + ]; + + for (const definition of wheelConnections) { + const anchor = new Entity(definition.left ? "UGV wheel left" : "UGV wheel right"); + anchor.setLocalPosition(definition.connection); + const wheelMesh = new Entity("UGV wheel mesh"); + wheelMesh.addComponent("render", { type: "cylinder", castShadows: false, receiveShadows: false }); + wheelMesh.setLocalEulerAngles(0, 0, 90); + wheelMesh.setLocalScale( + dimensions.wheelRadius * 2, + dimensions.wheelThickness, + dimensions.wheelRadius * 2, + ); + applyMaterial(wheelMesh, this.wheelMaterial); + anchor.addChild(wheelMesh); + vehicle.addChild(anchor); + this.wheelDefinitions.push({ ...definition, anchor }); + } + + vehicle.setLocalPosition(this.spawnPosition); + this.app.root.addChild(vehicle); + + const body = (vehicle as Entity & NativeRigidBodyAccess).rigidbody?.body; + if (!body) { + vehicle.destroy(); + throw new Error("Динамическое тело UGV не создалось."); + } + + const tuning = new this.ammo.btVehicleTuning(); + const raycaster = new this.ammo.btDefaultVehicleRaycaster(dynamicsWorld); + const nativeVehicle = new this.ammo.btRaycastVehicle(tuning, body, raycaster); + nativeVehicle.setCoordinateSystem(0, 1, 2); + + const axle = new this.ammo.btVector3(-1, 0, 0); + const direction = new this.ammo.btVector3(0, -1, 0); + const connection = new this.ammo.btVector3(0, 0, 0); + for (const definition of this.wheelDefinitions) { + connection.setValue(definition.connection.x, definition.connection.y, definition.connection.z); + const wheel = nativeVehicle.addWheel( + connection, + direction, + axle, + dimensions.suspensionRestLength, + dimensions.wheelRadius, + tuning, + definition.front, + ); + wheel.set_m_suspensionStiffness(24); + wheel.set_m_wheelsDampingRelaxation(3.2); + wheel.set_m_wheelsDampingCompression(4.8); + wheel.set_m_frictionSlip(5.5); + wheel.set_m_rollInfluence(0.08); + } + this.ammo.destroy(axle); + this.ammo.destroy(direction); + this.ammo.destroy(connection); + + dynamicsWorld.addAction(nativeVehicle); + this.vehicleEntity = vehicle; + this.vehicleTuning = tuning; + this.vehicleRaycaster = raycaster; + this.vehicle = nativeVehicle; + this.dynamicsWorld = dynamicsWorld; + } + + private updateCamera(deltaSeconds: number): void { + if (!this.vehicleEntity || !this.vehicle) return; + this.cameraTarget.copy(this.vehicleEntity.getPosition()); + this.cameraLookAt.copy(this.cameraTarget); + this.cameraLookAt.y += Math.max(0.15, this.settings.dimensionsMeters.height * 0.35); + + const nativeForward = this.vehicle.getForwardVector(); + this.forward.set(nativeForward.x(), 0, nativeForward.z()); + if (this.forward.lengthSq() < 0.001) this.forward.set(0, 0, 1); + this.forward.normalize(); + const rearOrbitYaw = Math.atan2(-this.forward.x, -this.forward.z); + if (!this.cameraOrbitInitialized) { + this.orbitYaw = rearOrbitYaw; + this.orbitPitch = DEFAULT_ORBIT_PITCH; + this.orbitDistance = Math.max(3.2, this.settings.dimensionsMeters.length * 3.8); + this.cameraOrbitInitialized = true; + this.orbitIdleSeconds = 0; + } else if (this.orbitPointerId === null) { + this.orbitIdleSeconds += Math.max(0, deltaSeconds); + if (this.orbitIdleSeconds >= CAMERA_RETURN_DELAY_SECONDS) { + const returnDelta = Math.max(0, deltaSeconds) / CAMERA_RETURN_DURATION_SECONDS; + this.orbitYaw = approachAngle(this.orbitYaw, rearOrbitYaw, Math.PI * returnDelta); + this.orbitPitch = approach( + this.orbitPitch, + DEFAULT_ORBIT_PITCH, + Math.PI * 0.5 * returnDelta, + ); + } + } + + const horizontalDistance = Math.cos(this.orbitPitch) * this.orbitDistance; + this.cameraDesired.set( + this.cameraLookAt.x + Math.sin(this.orbitYaw) * horizontalDistance, + this.cameraLookAt.y + Math.sin(this.orbitPitch) * this.orbitDistance, + this.cameraLookAt.z + Math.cos(this.orbitYaw) * horizontalDistance, + ); + + if (!this.cameraInitialized) { + this.smoothedCamera.copy(this.cameraDesired); + this.cameraInitialized = true; + } else { + const blend = 1 - Math.exp( + -Math.max(0, deltaSeconds) * (this.orbitPointerId === null ? 8 : 18), + ); + this.smoothedCamera.lerp(this.smoothedCamera, this.cameraDesired, blend); + } + this.camera.setPosition(this.smoothedCamera); + this.camera.lookAt(this.cameraLookAt); + } + + private findSafeSpawnPosition(): Vec3 { + const dimensions = vehicleDimensions(this.settings); + const footprint: ReadonlyArray = [ + [0, 0], + [dimensions.width * 0.42, dimensions.length * 0.42], + [-dimensions.width * 0.42, dimensions.length * 0.42], + [dimensions.width * 0.42, -dimensions.length * 0.42], + [-dimensions.width * 0.42, -dimensions.length * 0.42], + ]; + const clearanceDirections: ReadonlyArray = [ + [dimensions.width * 0.62, 0], + [-dimensions.width * 0.62, 0], + [0, dimensions.length * 0.62], + [0, -dimensions.length * 0.62], + ]; + const from = new Vec3(); + const to = new Vec3(); + const physics = (this.app as unknown as PhysicsSystemAccess).systems.rigidbody; + + for (const [offsetX, offsetZ] of UGV_SPAWN_OFFSETS) { + const heights: number[] = []; + let suitable = true; + for (const [sampleX, sampleZ] of footprint) { + const x = UGV_SPAWN_POSITION.x + offsetX + sampleX; + const z = UGV_SPAWN_POSITION.z + offsetZ + sampleZ; + from.set(x, 8, z); + to.set(x, -8, z); + const hit = physics.raycastFirst(from, to); + if (!hit || hit.normal.y < 0.72 || Math.abs(hit.point.y) > 2.5) { + suitable = false; + break; + } + heights.push(hit.point.y); + } + if (!suitable || heights.length !== footprint.length) continue; + const minimum = Math.min(...heights); + const maximum = Math.max(...heights); + if (maximum - minimum > 0.32) continue; + + const x = UGV_SPAWN_POSITION.x + offsetX; + const z = UGV_SPAWN_POSITION.z + offsetZ; + const ground = heights.reduce((sum, height) => sum + height, 0) / heights.length; + const clearanceHeight = ground + dimensions.originHeight; + const origin = new Vec3(x, clearanceHeight, z); + const blocked = clearanceDirections.some(([directionX, directionZ]) => { + to.set(x + directionX, clearanceHeight, z + directionZ); + return physics.raycastFirst(origin, to) !== null; + }); + if (!blocked) return new Vec3(x, ground + dimensions.originHeight, z); + } + + return UGV_SPAWN_POSITION.clone(); + } + + private destroyVehicle(): void { + if (this.vehicle && this.dynamicsWorld) { + runCleanup("remove vehicle action", () => this.dynamicsWorld?.removeAction(this.vehicle as NativeObject)); + } + if (this.vehicle) runCleanup("destroy vehicle", () => this.ammo.destroy(this.vehicle as NativeObject)); + if (this.vehicleRaycaster) runCleanup("destroy vehicle raycaster", () => this.ammo.destroy(this.vehicleRaycaster as NativeObject)); + if (this.vehicleTuning) runCleanup("destroy vehicle tuning", () => this.ammo.destroy(this.vehicleTuning as NativeObject)); + this.vehicle = null; + this.vehicleRaycaster = null; + this.vehicleTuning = null; + this.dynamicsWorld = null; + + if (this.vehicleEntity) runCleanup("destroy vehicle entity", () => this.vehicleEntity?.destroy()); + this.vehicleEntity = null; + this.wheelDefinitions.length = 0; + if (this.chassisMaterial) runCleanup("destroy chassis material", () => this.chassisMaterial?.destroy()); + if (this.wheelMaterial) runCleanup("destroy wheel material", () => this.wheelMaterial?.destroy()); + this.chassisMaterial = null; + this.wheelMaterial = null; + this.cameraInitialized = false; + } + + private isPressed(primary: string, alternative: string): boolean { + return this.pressed.has(primary) || this.pressed.has(alternative); + } + + private attachInput(): void { + window.addEventListener("keydown", this.onKeyDown, true); + window.addEventListener("keyup", this.onKeyUp, true); + window.addEventListener("blur", this.onWindowBlur); + this.canvas.addEventListener("pointerdown", this.onOrbitPointerDown); + this.canvas.addEventListener("pointermove", this.onOrbitPointerMove); + this.canvas.addEventListener("pointerup", this.onOrbitPointerUp); + this.canvas.addEventListener("pointercancel", this.onOrbitPointerUp); + this.canvas.addEventListener("wheel", this.onOrbitWheel, { passive: false }); + } + + private detachInput(): void { + window.removeEventListener("keydown", this.onKeyDown, true); + window.removeEventListener("keyup", this.onKeyUp, true); + window.removeEventListener("blur", this.onWindowBlur); + this.canvas.removeEventListener("pointerdown", this.onOrbitPointerDown); + this.canvas.removeEventListener("pointermove", this.onOrbitPointerMove); + this.canvas.removeEventListener("pointerup", this.onOrbitPointerUp); + this.canvas.removeEventListener("pointercancel", this.onOrbitPointerUp); + this.canvas.removeEventListener("wheel", this.onOrbitWheel); + this.releaseOrbitPointer(); + } + + private onOrbitPointerDown = (event: PointerEvent): void => { + if (!this.active || event.button !== 0 || this.orbitPointerId !== null) return; + event.preventDefault(); + this.orbitPointerId = event.pointerId; + this.orbitPointerX = event.clientX; + this.orbitPointerY = event.clientY; + this.orbitIdleSeconds = 0; + this.canvas.classList.add("is-orbiting"); + try { + this.canvas.setPointerCapture(event.pointerId); + } catch { + // A browser may already have released the pointer during a window transition. + } + }; + + private onOrbitPointerMove = (event: PointerEvent): void => { + if (!this.active || event.pointerId !== this.orbitPointerId) return; + event.preventDefault(); + const deltaX = event.clientX - this.orbitPointerX; + const deltaY = event.clientY - this.orbitPointerY; + this.orbitPointerX = event.clientX; + this.orbitPointerY = event.clientY; + this.orbitIdleSeconds = 0; + this.orbitYaw = (this.orbitYaw - deltaX * 0.006) % (Math.PI * 2); + this.orbitPitch = clamp(this.orbitPitch + deltaY * 0.0045, -0.12, 1.32); + }; + + private onOrbitPointerUp = (event: PointerEvent): void => { + if (event.pointerId !== this.orbitPointerId) return; + this.releaseOrbitPointer(); + }; + + private onOrbitWheel = (event: WheelEvent): void => { + if (!this.active) return; + event.preventDefault(); + this.orbitIdleSeconds = 0; + this.orbitDistance = clamp( + this.orbitDistance * Math.exp(event.deltaY * 0.0012), + 1.6, + 14, + ); + }; + + private releaseOrbitPointer(): void { + const pointerId = this.orbitPointerId; + this.orbitPointerId = null; + this.canvas.classList.remove("is-orbiting"); + if (pointerId === null || !this.canvas.hasPointerCapture(pointerId)) return; + try { + this.canvas.releasePointerCapture(pointerId); + } catch { + // Pointer capture is already gone; the local orbit state is still released. + } + } + + private onKeyDown = (event: KeyboardEvent): void => { + if (!this.active || isTextEntry(event.target)) return; + if (!UGV_KEYS.has(event.code)) return; + event.preventDefault(); + event.stopPropagation(); + if (event.code === "KeyR") { + this.reset(); + return; + } + this.pressed.add(event.code); + }; + + private onKeyUp = (event: KeyboardEvent): void => { + if (!this.active || !UGV_KEYS.has(event.code)) return; + event.preventDefault(); + event.stopPropagation(); + this.pressed.delete(event.code); + }; + + private onWindowBlur = (): void => { + this.pressed.clear(); + this.releaseOrbitPointer(); + }; +} + +const UGV_KEYS = new Set([ + "KeyW", + "KeyA", + "KeyS", + "KeyD", + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "Space", + "KeyR", +]); + +function createPhysicsProxyMesh( + app: Application, + source: Mesh, + localTransform: Mat4, + targetRatio: number, +): Mesh | null { + const primitive = source.primitive[0]; + const sourceVertexCount = source.vertexBuffer?.numVertices ?? 0; + if (!primitive || sourceVertexCount === 0 || primitive.count < 3) return null; + + const positions = new Float32Array(sourceVertexCount * 3); + const populatedVertices = source.getPositions(positions); + if (populatedVertices === 0) return null; + const transformed = new Float32Array(populatedVertices * 3); + const point = new Vec3(); + for (let vertex = 0; vertex < populatedVertices; vertex += 1) { + const offset = vertex * 3; + point.set(positions[offset], positions[offset + 1], positions[offset + 2]); + localTransform.transformPoint(point, point); + transformed[offset] = point.x; + transformed[offset + 1] = point.y; + transformed[offset + 2] = point.z; + } + + const requestedIndexCount = Math.max(3, primitive.count); + let indices = new Uint32Array(requestedIndexCount); + const populatedIndices = source.getIndices(indices); + if (populatedIndices === 0) { + const fallbackCount = Math.min(populatedVertices, requestedIndexCount); + indices = new Uint32Array(fallbackCount); + for (let index = 0; index < fallbackCount; index += 1) indices[index] = index; + } else if (populatedIndices !== indices.length) { + indices = indices.slice(0, populatedIndices); + } + const triangleIndexCount = Math.floor(indices.length / 3) * 3; + if (triangleIndexCount < 3) return null; + if (triangleIndexCount !== indices.length) indices = indices.slice(0, triangleIndexCount); + + if (targetRatio < 1) { + const targetIndexCount = Math.max(3, Math.floor(indices.length * targetRatio / 3) * 3); + const [simplifiedIndices] = MeshoptSimplifier.simplify( + indices, + transformed, + 3, + targetIndexCount, + PHYSICS_PROXY_ERROR, + ); + indices = new Uint32Array(simplifiedIndices); + } + + const compactedIndices = new Uint32Array(indices); + const [remap, compactedVertexCount] = MeshoptSimplifier.compactMesh(compactedIndices); + const compactedPositions = new Float32Array(compactedVertexCount * 3); + const missing = 0xffff_ffff; + for (let sourceVertex = 0; sourceVertex < remap.length; sourceVertex += 1) { + const compactedVertex = remap[sourceVertex]; + if (compactedVertex === missing) continue; + const sourceOffset = sourceVertex * 3; + const compactedOffset = compactedVertex * 3; + compactedPositions[compactedOffset] = transformed[sourceOffset]; + compactedPositions[compactedOffset + 1] = transformed[sourceOffset + 1]; + compactedPositions[compactedOffset + 2] = transformed[sourceOffset + 2]; + } + + const physicsMesh = new Mesh(app.graphicsDevice); + physicsMesh.setPositions(compactedPositions); + physicsMesh.setIndices(compactedIndices); + physicsMesh.update(PRIMITIVE_TRIANGLES, true); + return physicsMesh; +} + +function cloneUgvPreset(preset: SimulationUgvPreset): SimulationUgvPreset { + return { + ...preset, + dimensionsMeters: { ...preset.dimensionsMeters }, + }; +} + +function vehicleGeometryChanged( + current: SimulationUgvPreset, + next: SimulationUgvPreset, +): boolean { + return current.massKg !== next.massKg + || current.dimensionsMeters.length !== next.dimensionsMeters.length + || current.dimensionsMeters.width !== next.dimensionsMeters.width + || current.dimensionsMeters.height !== next.dimensionsMeters.height + || current.dimensionsMeters.groundClearance !== next.dimensionsMeters.groundClearance; +} + +function vehicleDimensions(settings: SimulationUgvPreset) { + const { length, width, height, groundClearance } = settings.dimensionsMeters; + const bodyHeight = height - groundClearance; + const wheelRadius = clamp( + groundClearance * 0.8, + 0.07, + Math.min(height * 0.4, length * 0.22), + ); + const wheelThickness = clamp(width * 0.13, 0.08, 0.16); + const bodyWidth = Math.max(0.12, width - wheelThickness * 2); + const mainHeight = bodyHeight * 0.68; + const equipmentHeight = bodyHeight - mainHeight; + const suspensionRestLength = clamp(groundClearance * 0.4, 0.04, 0.16); + const originHeight = groundClearance + bodyHeight / 2; + return { + length, + width, + bodyWidth, + mainHeight, + equipmentHeight, + mainCenterY: -bodyHeight / 2 + mainHeight / 2, + equipmentCenterY: bodyHeight / 2 - equipmentHeight / 2, + wheelRadius, + wheelThickness, + wheelX: width / 2 - wheelThickness / 2, + wheelZ: Math.max(0, length / 2 - wheelRadius), + wheelConnectionY: suspensionRestLength + wheelRadius - originHeight, + suspensionRestLength, + originHeight, + }; +} + +function createBox(name: string, scale: Vec3, material: StandardMaterial): Entity { + const entity = new Entity(name); + entity.addComponent("render", { type: "box", castShadows: false, receiveShadows: false }); + entity.setLocalScale(scale); + applyMaterial(entity, material); + return entity; +} + +function applyMaterial(entity: Entity, material: StandardMaterial): void { + for (const meshInstance of entity.render?.meshInstances ?? []) meshInstance.material = material; +} + +function createMaterial(diffuse: Color, emissive: Color): StandardMaterial { + const material = new StandardMaterial(); + material.diffuse = diffuse; + material.emissive = emissive; + material.metalness = 0.15; + material.gloss = 0.32; + material.update(); + return material; +} + +function readThemeAccent(): Color { + const raw = getComputedStyle(document.documentElement).getPropertyValue("--nodedc-accent-rgb"); + const channels = raw.trim().split(/[\s,]+/).map(Number); + if (channels.length < 3 || channels.some((value) => !Number.isFinite(value))) { + return new Color(0.22, 0.72, 0.38); + } + return new Color(channels[0] / 255, channels[1] / 255, channels[2] / 255); +} + +function isTextEntry(target: EventTarget | null): boolean { + return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement + || target instanceof HTMLSelectElement || target instanceof HTMLButtonElement + || (target instanceof HTMLElement && target.isContentEditable); +} + +function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); } +function approach(current: number, target: number, maximumDelta: number): number { + if (current < target) return Math.min(target, current + maximumDelta); + if (current > target) return Math.max(target, current - maximumDelta); + return target; +} +function approachAngle(current: number, target: number, maximumDelta: number): number { + const delta = ((target - current + Math.PI) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2) - Math.PI; + return current + clamp(delta, -maximumDelta, maximumDelta); +} +function runCleanup(label: string, cleanup: () => void): void { + try { + cleanup(); + } catch (error) { + console.warn(`[Simulation UGV] Cleanup failed: ${label}`, error); + } +} + +function nextAnimationFrame(): Promise { return new Promise((resolve) => requestAnimationFrame(() => resolve())); } diff --git a/apps/control-station/src/components/simulation/SimulationUgvSettingsPanel.tsx b/apps/control-station/src/components/simulation/SimulationUgvSettingsPanel.tsx new file mode 100644 index 0000000..49a0a07 --- /dev/null +++ b/apps/control-station/src/components/simulation/SimulationUgvSettingsPanel.tsx @@ -0,0 +1,191 @@ +import { + Button, + Icon, + RangeControl, + Switch, + TextField, +} from "@nodedc/ui-react"; + +import { + SIMULATION_UGV_EXACT_VALUE_LIMITS, + type SimulationUgvPreset, +} from "../../core/simulation/projects"; + +export type SimulationUgvPresetSaveState = "idle" | "saving" | "saved" | "failed"; + +export function SimulationUgvSettingsPanel({ + value, + disabled, + saveState, + onChange, + onSave, +}: { + value: SimulationUgvPreset; + disabled: boolean; + saveState: SimulationUgvPresetSaveState; + onChange: (preset: SimulationUgvPreset) => void; + onSave: () => void; +}) { + const dimensions = value.dimensionsMeters; + const updateDimensions = ( + patch: Partial, + ) => { + const next = { ...dimensions, ...patch }; + next.groundClearance = Math.max( + SIMULATION_UGV_EXACT_VALUE_LIMITS.groundClearance.min, + Math.min(next.groundClearance, roundToHundredth(next.height - 0.05)), + ); + onChange({ ...value, dimensionsMeters: next }); + }; + + return ( +
+
+ Физика UGV + Дифференциальный привод +
+

+ A/D без W/S разворачивают UGV вокруг собственного центра. Скорость и + динамика применяются сразу. Мышь вращает камеру вокруг машины, колесо + меняет дистанцию; после паузы камера плавно возвращается назад. Ползунок + работает в штатном диапазоне, точное значение можно ввести вручную. +

+ onChange({ ...value, presetName: event.currentTarget.value })} + /> + `${Math.round(mass)} кг`} + onChange={(massKg) => onChange({ ...value, massKg })} + /> +
+ Габариты + В метрах +
+ updateDimensions({ length })} + /> + updateDimensions({ width })} + /> + updateDimensions({ height })} + /> + updateDimensions({ groundClearance })} + /> +
+ Динамика + Ограничители движения +
+ `${formatDecimal(speed, 1)} м/с`} + onChange={(maxSpeedMetersPerSecond) => onChange({ + ...value, + maxSpeedMetersPerSecond, + })} + /> + `${Math.round(rate)}°/с`} + onChange={(maxTurnRateDegrees) => onChange({ ...value, maxTurnRateDegrees })} + /> + onChange({ ...value, invertSteering })} + /> + +
+ ); +} + +function formatMeters(value: number): string { + return `${formatDecimal(value, 2)} м`; +} + +function formatDecimal(value: number, digits: number): string { + return value.toFixed(digits).replace(".", ","); +} + +function roundToHundredth(value: number): number { + return Math.round(value * 100) / 100; +} diff --git a/apps/control-station/src/components/simulation/SimulationViewport.tsx b/apps/control-station/src/components/simulation/SimulationViewport.tsx index 7ba3d2b..13a945d 100644 --- a/apps/control-station/src/components/simulation/SimulationViewport.tsx +++ b/apps/control-station/src/components/simulation/SimulationViewport.tsx @@ -2,18 +2,21 @@ import { useEffect, useId, useRef, useState } from "react"; import { ActivityIndicator, Button, + GlassMaterialSurface, Icon, IconButton, SegmentedControl, Select, StatusBadge, Switch, + TextField, } from "@nodedc/ui-react"; import { saveSimulationViewerSettings, + type SimulationEulerRotation, type SimulationProject, - type SimulationTransformAxis, + type SimulationUgvPreset, type SimulationViewerSettings, } from "../../core/simulation/projects"; import { @@ -22,16 +25,17 @@ import { } from "../../core/runtime/latestAsyncCommitter"; import { PlayCanvasRuntime, - playCanvasAxisTransform, + playCanvasEulerTransform, + type SimulationControlMode, type SimulationQuality, type SimulationViewMode, } from "./PlayCanvasRuntime"; +import { + SimulationUgvSettingsPanel, + type SimulationUgvPresetSaveState, +} from "./SimulationUgvSettingsPanel"; -const AXIS_OPTIONS: Array<{ value: SimulationTransformAxis; label: string }> = [ - { value: "x", label: "X" }, - { value: "y", label: "Y" }, - { value: "z", label: "Z" }, -]; +const ROTATION_AXES = ["x", "y", "z"] as const; export function SimulationViewport({ project, @@ -44,23 +48,34 @@ export function SimulationViewport({ const runtimeRef = useRef(null); const viewerSettingsRef = useRef(project.viewerSettings); const settingsCommitterRef = useRef | null>(null); + const settingsCommitSucceededRef = useRef(true); + const ugvSaveRequestRef = useRef(0); + const controlRequestRef = useRef(0); const settingsId = useId(); const [state, setState] = useState<"mounting" | "loading" | "ready" | "failed">("mounting"); const [error, setError] = useState(null); const [viewMode, setViewMode] = useState("visual"); + const [controlMode, setControlMode] = useState("free"); + const [controlState, setControlState] = useState<"idle" | "loading" | "ready" | "failed">("idle"); const [quality, setQuality] = useState(project.viewerSettings.quality); const [collisionState, setCollisionState] = useState<"idle" | "loading" | "ready" | "failed">("idle"); const [settingsOpen, setSettingsOpen] = useState(false); - const [visualInverted, setVisualInverted] = useState(project.viewerSettings.visual.inverted); - const [visualAxis, setVisualAxis] = useState(project.viewerSettings.visual.axis); - const [collisionInverted, setCollisionInverted] = useState(project.viewerSettings.collision.inverted); - const [collisionAxis, setCollisionAxis] = useState(project.viewerSettings.collision.axis); + const [visualRotation, setVisualRotation] = useState({ + ...project.viewerSettings.visual.rotationDegrees, + }); + const [collisionRotation, setCollisionRotation] = useState({ + ...project.viewerSettings.collision.rotationDegrees, + }); const [horizontalInverted, setHorizontalInverted] = useState( project.viewerSettings.camera.invertHorizontal, ); const [verticalInverted, setVerticalInverted] = useState( project.viewerSettings.camera.invertVertical, ); + const [ugvDraft, setUgvDraft] = useState(() => + cloneUgvPreset(project.viewerSettings.ugv), + ); + const [ugvSaveState, setUgvSaveState] = useState("idle"); const [settingsError, setSettingsError] = useState(null); const worldManifestRevision = JSON.stringify(project.worldManifest); @@ -68,14 +83,18 @@ export function SimulationViewport({ const settings = project.viewerSettings; viewerSettingsRef.current = settings; setQuality(settings.quality); - setVisualInverted(settings.visual.inverted); - setVisualAxis(settings.visual.axis); - setCollisionInverted(settings.collision.inverted); - setCollisionAxis(settings.collision.axis); + setVisualRotation({ ...settings.visual.rotationDegrees }); + setCollisionRotation({ ...settings.collision.rotationDegrees }); setHorizontalInverted(settings.camera.invertHorizontal); setVerticalInverted(settings.camera.invertVertical); + setUgvDraft(cloneUgvPreset(settings.ugv)); + setUgvSaveState("idle"); + ugvSaveRequestRef.current += 1; setSettingsError(null); setSettingsOpen(false); + controlRequestRef.current += 1; + setControlMode("free"); + setControlState("idle"); }, [project.projectId]); useEffect(() => { @@ -84,9 +103,11 @@ export function SimulationViewport({ async commit(settings) { try { const saved = await saveSimulationViewerSettings(project.projectId, settings); + settingsCommitSucceededRef.current = true; if (active) onProjectChange?.(saved); return true; } catch (caught) { + settingsCommitSucceededRef.current = false; if (active) { setSettingsError( caught instanceof Error ? caught.message : "Не удалось сохранить настройки сцены.", @@ -122,15 +143,16 @@ export function SimulationViewport({ runtime.setQuality(settings.quality); runtime.setCameraInversion("horizontal", settings.camera.invertHorizontal); runtime.setCameraInversion("vertical", settings.camera.invertVertical); + runtime.setUgvPreset(settings.ugv); setState("loading"); await runtime.loadWorld(manifest); runtime.setLayerWorldTransform( "visual", - playCanvasAxisTransform(settings.visual.axis, settings.visual.inverted), + playCanvasEulerTransform(settings.visual.rotationDegrees), ); runtime.setLayerWorldTransform( "collision", - playCanvasAxisTransform(settings.collision.axis, settings.collision.inverted), + playCanvasEulerTransform(settings.collision.rotationDegrees), ); if (!cancelled) setState("ready"); }).catch((caught: unknown) => { @@ -140,6 +162,7 @@ export function SimulationViewport({ }); return () => { cancelled = true; + controlRequestRef.current += 1; runtime.dispose(); runtimeRef.current = null; }; @@ -151,25 +174,89 @@ export function SimulationViewport({ const collisionAvailable = project.worldManifest?.collision.available ?? false; const commitViewerSettings = (settings: SimulationViewerSettings) => { viewerSettingsRef.current = settings; + settingsCommitSucceededRef.current = false; settingsCommitterRef.current?.enqueue(settings); }; + const changeUgvDraft = (next: SimulationUgvPreset) => { + setUgvDraft(cloneUgvPreset(next)); + setUgvSaveState("idle"); + ugvSaveRequestRef.current += 1; + runtimeRef.current?.setUgvPreset(next); + }; + + const saveUgvPreset = () => { + const committer = settingsCommitterRef.current; + if (!committer) { + setUgvSaveState("failed"); + return; + } + const request = ++ugvSaveRequestRef.current; + const normalized = cloneUgvPreset({ + ...ugvDraft, + presetName: ugvDraft.presetName.trim(), + }); + setUgvDraft(normalized); + setUgvSaveState("saving"); + setSettingsError(null); + settingsCommitSucceededRef.current = false; + const settings = { ...viewerSettingsRef.current, ugv: normalized }; + viewerSettingsRef.current = settings; + committer.enqueue(settings); + void committer.waitForIdle().then(() => { + if (request !== ugvSaveRequestRef.current) return; + setUgvSaveState(settingsCommitSucceededRef.current ? "saved" : "failed"); + }); + }; + + const changeControlMode = (next: SimulationControlMode) => { + const runtime = runtimeRef.current; + if (!runtime) return; + const request = ++controlRequestRef.current; + setControlMode(next); + setControlState(next === "ugv" ? "loading" : "idle"); + if (next === "ugv" && collisionState !== "ready") setCollisionState("loading"); + void runtime.setControlMode(next).then(() => { + if (request !== controlRequestRef.current) return; + if (next === "ugv") { + setControlState("ready"); + setCollisionState("ready"); + } + }).catch((caught: unknown) => { + if (request !== controlRequestRef.current) return; + setControlMode("free"); + setControlState("failed"); + setCollisionState("failed"); + setError(caught instanceof Error ? caught.message : "Не удалось запустить UGV."); + }); + }; + return ( -
+
- {collisionState === "loading" - ? "Загрузка collision" + {controlState === "loading" + ? "Подготовка UGV" + : controlState === "ready" + ? "UGV готов" + : controlState === "failed" + ? "Ошибка UGV" + : collisionState === "loading" + ? "Загрузка коллизий" : collisionState === "failed" - ? "Ошибка collision" + ? "Ошибка коллизий" : state === "ready" ? "Runtime готов" : state === "failed" ? "Ошибка runtime" - : "Загрузка сцены"} + : "Загрузка локации"} - PlayCanvas Engine 2.21.4 + Engine 2.21.4
{ setCollisionState("failed"); - setError(caught instanceof Error ? caught.message : "Не удалось открыть collision GLB."); + setError(caught instanceof Error ? caught.message : "Не удалось открыть слой коллизий."); }); }} items={[ @@ -227,7 +335,7 @@ export function SimulationViewport({ {settingsOpen ? ( - -
+
+
Система координат - Мир PlayCanvas · Y вверх + Мир сцены · Y вверх

Коррекция слоёв не меняет координаты камеры, навигации и будущей физики.

-
-
- { - setVisualInverted(checked); - runtimeRef.current?.setLayerWorldTransform( - "visual", - playCanvasAxisTransform(visualAxis, checked), - ); - commitViewerSettings({ - ...viewerSettingsRef.current, - visual: { ...viewerSettingsRef.current.visual, inverted: checked }, - }); - }} - /> - { - setCollisionAxis(axis); - runtimeRef.current?.setLayerWorldTransform( - "collision", - playCanvasAxisTransform(axis, collisionInverted), - ); - commitViewerSettings({ - ...viewerSettingsRef.current, - collision: { ...viewerSettingsRef.current.collision, axis }, - }); - }} - /> -
+
+ { + setVisualRotation(next); + runtimeRef.current?.setLayerWorldTransform( + "visual", + playCanvasEulerTransform(next), + ); + commitViewerSettings({ + ...viewerSettingsRef.current, + visual: { rotationDegrees: next }, + }); + }} + /> + { + setCollisionRotation(next); + runtimeRef.current?.setLayerWorldTransform( + "collision", + playCanvasEulerTransform(next), + ); + commitViewerSettings({ + ...viewerSettingsRef.current, + collision: { rotationDegrees: next }, + }); + }} + />
-
-
+
+
Управление камерой Мышь и тачпад @@ -362,39 +427,165 @@ export function SimulationViewport({ }} />
-
- {settingsError ? ( -

- Настройки не сохранены: {settingsError} -

- ) : null} -
+
+ + {settingsError ? ( +

+ Настройки не сохранены: {settingsError} +

+ ) : null} +
+ ) : null}
- + {state !== "ready" ? (
- {state === "failed" ? null : } - {state === "failed" ? "Сцена не открылась" : "PlayCanvas загружает мир"} -

{error ?? "Streamed SOG догружается по мере готовности runtime."}

+ {state === "failed" ? null : } + {state === "failed" ? "Локация не открылась" : "Загружаем локацию"} +

{error ?? "LOD-уровни загружаются по мере готовности runtime."}

) : null}
{!collisionAvailable ? (
- Collision недоступен - Для этой сборки collision GLB не был запрошен; визуальный слой настоящий и не подменяется. + Коллизии недоступны + Для этой сборки слой коллизий не создавался; визуальный слой остаётся доступен.
) : collisionState === "failed" ? (
- Collision не открылся - {error ?? "PlayCanvas не смог загрузить collision GLB."} + Коллизии не открылись + {error ?? "Слой коллизий не загрузился."}
) : null}
); } + +function cloneUgvPreset(preset: SimulationUgvPreset): SimulationUgvPreset { + return { + ...preset, + dimensionsMeters: { ...preset.dimensionsMeters }, + }; +} + +function LayerRotationFields({ + label, + value, + disabled, + onChange, +}: { + label: string; + value: SimulationEulerRotation; + disabled: boolean; + onChange: (rotation: SimulationEulerRotation) => void; +}) { + return ( +
+ {label} +
+ {ROTATION_AXES.map((axis) => ( + onChange({ ...value, [axis]: angle })} + /> + ))} +
+
+ ); +} + +function RotationField({ + label, + ariaLabel, + value, + disabled, + onChange, +}: { + label: string; + ariaLabel: string; + value: number; + disabled: boolean; + onChange: (angle: number) => void; +}) { + const [draft, setDraft] = useState(() => String(value)); + const editingRef = useRef(false); + const cancelNextBlurRef = useRef(false); + + useEffect(() => { + if (!editingRef.current) setDraft(String(value)); + }, [value]); + + const parsedDraft = () => Number(draft.trim().replace(",", ".")); + const finishEditing = (commit: boolean) => { + const parsed = parsedDraft(); + if (commit && draft.trim() !== "" && Number.isFinite(parsed)) { + const normalized = Math.max(-360, Math.min(360, parsed)); + onChange(normalized); + setDraft(String(normalized)); + } else { + setDraft(String(value)); + } + editingRef.current = false; + }; + + return ( + { + editingRef.current = true; + }} + onChange={(event) => { + // Treat the first input event as the start of editing as well. Programmatic + // focus (and some browser/trackpad paths) may not deliver React's focus + // event before the controlled value changes. + editingRef.current = true; + const nextDraft = event.currentTarget.value; + setDraft(nextDraft); + const parsed = Number(nextDraft.trim().replace(",", ".")); + if (nextDraft.trim() !== "" && Number.isFinite(parsed) && parsed >= -360 && parsed <= 360) { + onChange(parsed); + } + }} + onKeyDown={(event) => { + event.stopPropagation(); + if (event.key === "Enter") event.currentTarget.blur(); + if (event.key === "Escape") { + event.preventDefault(); + cancelNextBlurRef.current = true; + finishEditing(false); + event.currentTarget.blur(); + } + }} + onBlur={() => { + if (cancelNextBlurRef.current) { + cancelNextBlurRef.current = false; + return; + } + finishEditing(true); + }} + /> + ); +} diff --git a/apps/control-station/src/core/simulation/projects.ts b/apps/control-station/src/core/simulation/projects.ts index 99982d3..48d538d 100644 --- a/apps/control-station/src/core/simulation/projects.ts +++ b/apps/control-station/src/core/simulation/projects.ts @@ -1,6 +1,10 @@ export type SimulationSceneType = "interior" | "outdoor" | "object"; -export type SimulationTransformAxis = "x" | "y" | "z"; export type SimulationViewerQuality = "low" | "medium" | "high" | "ultra" | "maximum"; +export interface SimulationEulerRotation { + x: number; + y: number; + z: number; +} export type SimulationProjectStatus = | "uploading" | "queued" @@ -35,13 +39,38 @@ export interface SimulationWorldManifest { } export interface SimulationViewerSettings { - schemaVersion: "missioncore.simulation-viewer-settings/v1"; + schemaVersion: "missioncore.simulation-viewer-settings/v3"; quality: SimulationViewerQuality; - visual: { inverted: boolean; axis: SimulationTransformAxis }; - collision: { inverted: boolean; axis: SimulationTransformAxis }; + visual: { rotationDegrees: SimulationEulerRotation }; + collision: { rotationDegrees: SimulationEulerRotation }; camera: { invertHorizontal: boolean; invertVertical: boolean }; + ugv: SimulationUgvPreset; } +export interface SimulationUgvPreset { + presetName: string; + massKg: number; + dimensionsMeters: { + length: number; + width: number; + height: number; + groundClearance: number; + }; + maxSpeedMetersPerSecond: number; + maxTurnRateDegrees: number; + invertSteering: boolean; +} + +export const SIMULATION_UGV_EXACT_VALUE_LIMITS = { + massKg: { min: 0.01, max: 1_000_000_000 }, + length: { min: 0.01, max: 1_000_000_000 }, + width: { min: 0.01, max: 1_000_000_000 }, + height: { min: 0.07, max: 1_000_000_000 }, + groundClearance: { min: 0.01, max: 1_000_000_000 }, + maxSpeedMetersPerSecond: { min: 0, max: 1_000_000_000 }, + maxTurnRateDegrees: { min: 0, max: 1_000_000_000 }, +} as const; + export interface SimulationProject { schemaVersion: "missioncore.simulation-project/v1"; projectId: string; @@ -58,7 +87,9 @@ export interface SimulationProject { provider: { providerId: "gaussian-pipeline"; jobId: string | null; + jobCreatedAtUtc: string | null; state: string | null; + stateStartedAtUtc: string | null; progress: { completed_steps?: number; total_steps?: number; stage?: string } | null; runtime: { source_revision?: string; image_digest?: string } | null; }; @@ -85,11 +116,60 @@ export interface SimulationUploadProgress { uploadedBytes: number; totalBytes: number; currentPath: string; + bytesPerSecond: number | null; + estimatedSecondsRemaining: number | null; } const API_ROOT = "/api/v1/simulation-worlds/projects"; const CHUNK_BYTES = 8 * 1024 * 1024; const MAX_UPLOAD_ATTEMPTS = 3; +const REFERENCE_SOURCE_BYTES = 4_100_808_854; +const REFERENCE_PREPARATION_SECONDS = 187; +const REFERENCE_STREAM_SECONDS = 2_317; + +export interface SimulationProcessingEstimate { + percent: number; + estimatedSecondsRemaining: number | null; +} + +export function estimateSimulationProcessing( + project: SimulationProject, + nowMilliseconds = Date.now(), +): SimulationProcessingEstimate | null { + if (project.status !== "processing") return null; + const jobStarted = Date.parse(project.provider.jobCreatedAtUtc ?? ""); + if (!Number.isFinite(jobStarted)) return null; + const elapsedSeconds = Math.max(0, (nowMilliseconds - jobStarted) / 1_000); + const sourceScale = clamp( + project.source.totalByteLength / REFERENCE_SOURCE_BYTES, + 0.1, + 4, + ); + let expectedSeconds = ( + REFERENCE_PREPARATION_SECONDS + REFERENCE_STREAM_SECONDS + ) * sourceScale; + if (project.provider.state === "building_streamed_sog") { + const streamedStarted = Date.parse(project.provider.stateStartedAtUtc ?? ""); + if (Number.isFinite(streamedStarted) && streamedStarted >= jobStarted) { + const preparationSeconds = Math.max(1, (streamedStarted - jobStarted) / 1_000); + const observedLoad = clamp( + preparationSeconds / (REFERENCE_PREPARATION_SECONDS * sourceScale), + 0.5, + 2, + ); + expectedSeconds = preparationSeconds + + REFERENCE_STREAM_SECONDS * sourceScale * observedLoad; + } + } + const rawPercent = expectedSeconds > 0 ? elapsedSeconds / expectedSeconds * 100 : 0; + if (rawPercent >= 100) { + return { percent: 99, estimatedSecondsRemaining: null }; + } + return { + percent: Math.max(1, Math.floor(rawPercent)), + estimatedSecondsRemaining: Math.max(0, Math.ceil(expectedSeconds - elapsedSeconds)), + }; +} export async function fetchSimulationProjects(signal?: AbortSignal): Promise { const response = await fetch(API_ROOT, { signal, cache: "no-store" }); @@ -107,6 +187,15 @@ export async function createAndUploadSimulationProject( sceneType: SimulationSceneType, candidates: SimulationUploadCandidate[], onProgress: (progress: SimulationUploadProgress) => void, +): Promise { + const project = await createSimulationProject(name, sceneType, candidates); + return uploadSimulationProjectSource(project, candidates, onProgress); +} + +export async function createSimulationProject( + name: string, + sceneType: SimulationSceneType, + candidates: SimulationUploadCandidate[], ): Promise { const sourceKind = sourceKindFor(candidates); const createdResponse = await fetch(API_ROOT, { @@ -123,36 +212,85 @@ export async function createAndUploadSimulationProject( })), }), }); - let project = parseProject(await jsonResponse(createdResponse)); + return parseProject(await jsonResponse(createdResponse)); +} + +export async function uploadSimulationProjectSource( + project: SimulationProject, + candidates: SimulationUploadCandidate[], + onProgress: (progress: SimulationUploadProgress) => void, + signal?: AbortSignal, +): Promise { const byPath = new Map(candidates.map((candidate) => [candidate.logicalPath, candidate.file])); const totalBytes = candidates.reduce((total, candidate) => total + candidate.file.size, 0); let confirmedBytes = 0; + let sampleBytes = 0; + let sampleAt = performance.now(); + let smoothedBytesPerSecond: number | null = null; + const reportProgress = (currentPath: string) => { + const remainingBytes = Math.max(0, totalBytes - confirmedBytes); + onProgress({ + uploadedBytes: confirmedBytes, + totalBytes, + currentPath, + bytesPerSecond: smoothedBytesPerSecond, + estimatedSecondsRemaining: smoothedBytesPerSecond && remainingBytes > 0 + ? Math.ceil(remainingBytes / smoothedBytesPerSecond) + : remainingBytes === 0 ? 0 : null, + }); + }; for (const sourceFile of project.source.files) { + signal?.throwIfAborted(); const file = byPath.get(sourceFile.logicalPath); if (!file) throw new Error(`Сервер изменил состав источника: ${sourceFile.logicalPath}`); const uploadUrl = `${API_ROOT}/${encodeURIComponent(project.projectId)}/source/${encodeURIComponent(sourceFile.fileId)}`; - let offset = await readUploadOffset(uploadUrl, file.size); + let offset = await readUploadOffset(uploadUrl, file.size, signal); confirmedBytes += offset; - onProgress({ uploadedBytes: confirmedBytes, totalBytes, currentPath: sourceFile.logicalPath }); + sampleBytes = confirmedBytes; + sampleAt = performance.now(); + reportProgress(sourceFile.logicalPath); while (offset < file.size) { + signal?.throwIfAborted(); const nextOffset = Math.min(file.size, offset + CHUNK_BYTES); - const confirmedOffset = await uploadChunk(uploadUrl, file, offset, nextOffset); + const confirmedOffset = await uploadChunk(uploadUrl, file, offset, nextOffset, signal); confirmedBytes += confirmedOffset - offset; offset = confirmedOffset; - onProgress({ uploadedBytes: confirmedBytes, totalBytes, currentPath: sourceFile.logicalPath }); + const now = performance.now(); + const elapsedSeconds = (now - sampleAt) / 1_000; + const transferredBytes = confirmedBytes - sampleBytes; + if (elapsedSeconds > 0 && transferredBytes > 0) { + const currentBytesPerSecond = transferredBytes / elapsedSeconds; + smoothedBytesPerSecond = smoothedBytesPerSecond === null + ? currentBytesPerSecond + : smoothedBytesPerSecond * 0.7 + currentBytesPerSecond * 0.3; + } + sampleBytes = confirmedBytes; + sampleAt = now; + reportProgress(sourceFile.logicalPath); } } - const buildResponse = await fetch(`${API_ROOT}/${encodeURIComponent(project.projectId)}/build`, { + return startSimulationProjectBuild(project.projectId, signal); +} + +export async function startSimulationProjectBuild( + projectId: string, + signal?: AbortSignal, +): Promise { + const buildResponse = await fetch(`${API_ROOT}/${encodeURIComponent(projectId)}/build`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", + signal, }); - project = parseProject(await jsonResponse(buildResponse)); - return project; + return parseProject(await jsonResponse(buildResponse)); } -async function readUploadOffset(uploadUrl: string, fileSize: number): Promise { - const head = await fetch(uploadUrl, { method: "HEAD", cache: "no-store" }); +async function readUploadOffset( + uploadUrl: string, + fileSize: number, + signal?: AbortSignal, +): Promise { + const head = await fetch(uploadUrl, { method: "HEAD", cache: "no-store", signal }); if (!head.ok) await jsonResponse(head); const value = head.headers.get("Upload-Offset"); if (!value || !/^\d+$/.test(value)) { @@ -170,11 +308,13 @@ async function uploadChunk( file: File, offset: number, nextOffset: number, + signal?: AbortSignal, ): Promise { let lastFailure: unknown = null; for (let attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt += 1) { let response: Response | null = null; try { + signal?.throwIfAborted(); response = await fetch(uploadUrl, { method: "PATCH", headers: { @@ -182,8 +322,10 @@ async function uploadChunk( "Upload-Offset": String(offset), }, body: file.slice(offset, nextOffset), + signal, }); } catch (caught) { + if (signal?.aborted) throw caught; lastFailure = caught; } if (response?.ok) { @@ -203,7 +345,7 @@ async function uploadChunk( lastFailure = new Error(`Mission Core временно вернул HTTP ${response.status}.`); } try { - const confirmed = await readUploadOffset(uploadUrl, file.size); + const confirmed = await readUploadOffset(uploadUrl, file.size, signal); if (confirmed === nextOffset) return confirmed; if (confirmed !== offset) { throw new Error("Сервер подтвердил неожиданное смещение загрузки."); @@ -246,26 +388,32 @@ export async function saveSimulationViewerSettings( body: JSON.stringify({ schema_version: settings.schemaVersion, quality: settings.quality, - visual: settings.visual, - collision: settings.collision, + visual: { rotation_degrees: settings.visual.rotationDegrees }, + collision: { rotation_degrees: settings.collision.rotationDegrees }, camera: { invert_horizontal: settings.camera.invertHorizontal, invert_vertical: settings.camera.invertVertical, }, + ugv: { + preset_name: settings.ugv.presetName, + mass_kg: settings.ugv.massKg, + dimensions_m: { + length: settings.ugv.dimensionsMeters.length, + width: settings.ugv.dimensionsMeters.width, + height: settings.ugv.dimensionsMeters.height, + ground_clearance: settings.ugv.dimensionsMeters.groundClearance, + }, + max_speed_mps: settings.ugv.maxSpeedMetersPerSecond, + max_turn_rate_degrees: settings.ugv.maxTurnRateDegrees, + invert_steering: settings.ugv.invertSteering, + }, }), }, ); return parseProject(await jsonResponse(response)); } -export async function retrySimulationProject(projectId: string): Promise { - const response = await fetch(`${API_ROOT}/${encodeURIComponent(projectId)}/build`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: "{}", - }); - return parseProject(await jsonResponse(response)); -} +export const retrySimulationProject = startSimulationProjectBuild; export async function deleteSimulationProject(projectId: string): Promise { const response = await fetch(`${API_ROOT}/${encodeURIComponent(projectId)}`, { method: "DELETE" }); @@ -321,7 +469,9 @@ function parseProject(value: unknown): SimulationProject { provider: { providerId: provider.provider_id === "gaussian-pipeline" ? "gaussian-pipeline" : invalid("provider id"), jobId: nullableString(provider.job_id, "provider job id"), + jobCreatedAtUtc: nullableString(provider.job_created_at_utc ?? null, "provider job creation"), state: nullableString(provider.state, "provider state"), + stateStartedAtUtc: nullableString(provider.state_started_at_utc ?? null, "provider state start"), progress: provider.progress === null ? null : objectValue(provider.progress, "provider progress"), runtime: provider.runtime === null ? null : objectValue(provider.runtime, "provider runtime"), }, @@ -345,16 +495,17 @@ function parseProject(value: unknown): SimulationProject { function parseViewerSettings(value: unknown): SimulationViewerSettings { const record = objectValue(value, "viewer settings"); - exactKeys(record, ["schema_version", "quality", "visual", "collision", "camera"], "viewer settings"); - if (record.schema_version !== "missioncore.simulation-viewer-settings/v1") { + exactKeys(record, ["schema_version", "quality", "visual", "collision", "camera", "ugv"], "viewer settings"); + if (record.schema_version !== "missioncore.simulation-viewer-settings/v3") { throw new Error("Настройки сцены вернули неподдерживаемую версию."); } const visual = parseLayerViewerSettings(record.visual, "visual settings"); const collision = parseLayerViewerSettings(record.collision, "collision settings"); const camera = objectValue(record.camera, "camera settings"); exactKeys(camera, ["invert_horizontal", "invert_vertical"], "camera settings"); + const ugv = parseUgvPreset(record.ugv); return { - schemaVersion: "missioncore.simulation-viewer-settings/v1", + schemaVersion: "missioncore.simulation-viewer-settings/v3", quality: qualityValue(record.quality), visual, collision, @@ -362,18 +513,88 @@ function parseViewerSettings(value: unknown): SimulationViewerSettings { invertHorizontal: booleanValue(camera.invert_horizontal, "horizontal camera inversion"), invertVertical: booleanValue(camera.invert_vertical, "vertical camera inversion"), }, + ugv, + }; +} + +function parseUgvPreset(value: unknown): SimulationUgvPreset { + const record = objectValue(value, "UGV settings"); + exactKeys( + record, + ["preset_name", "mass_kg", "dimensions_m", "max_speed_mps", "max_turn_rate_degrees", "invert_steering"], + "UGV settings", + ); + const dimensions = objectValue(record.dimensions_m, "UGV dimensions"); + exactKeys(dimensions, ["length", "width", "height", "ground_clearance"], "UGV dimensions"); + const presetName = stringValue(record.preset_name, "UGV preset name"); + if (presetName.length > 80) return invalid("UGV preset name"); + const height = finiteNumberValue( + dimensions.height, + SIMULATION_UGV_EXACT_VALUE_LIMITS.height.min, + SIMULATION_UGV_EXACT_VALUE_LIMITS.height.max, + "UGV height", + ); + const groundClearance = finiteNumberValue( + dimensions.ground_clearance, + SIMULATION_UGV_EXACT_VALUE_LIMITS.groundClearance.min, + SIMULATION_UGV_EXACT_VALUE_LIMITS.groundClearance.max, + "UGV ground clearance", + ); + if (groundClearance >= height - 0.05) return invalid("UGV ground clearance"); + return { + presetName, + massKg: finiteNumberValue( + record.mass_kg, + SIMULATION_UGV_EXACT_VALUE_LIMITS.massKg.min, + SIMULATION_UGV_EXACT_VALUE_LIMITS.massKg.max, + "UGV mass", + ), + dimensionsMeters: { + length: finiteNumberValue( + dimensions.length, + SIMULATION_UGV_EXACT_VALUE_LIMITS.length.min, + SIMULATION_UGV_EXACT_VALUE_LIMITS.length.max, + "UGV length", + ), + width: finiteNumberValue( + dimensions.width, + SIMULATION_UGV_EXACT_VALUE_LIMITS.width.min, + SIMULATION_UGV_EXACT_VALUE_LIMITS.width.max, + "UGV width", + ), + height, + groundClearance, + }, + maxSpeedMetersPerSecond: finiteNumberValue( + record.max_speed_mps, + SIMULATION_UGV_EXACT_VALUE_LIMITS.maxSpeedMetersPerSecond.min, + SIMULATION_UGV_EXACT_VALUE_LIMITS.maxSpeedMetersPerSecond.max, + "UGV maximum speed", + ), + maxTurnRateDegrees: finiteNumberValue( + record.max_turn_rate_degrees, + SIMULATION_UGV_EXACT_VALUE_LIMITS.maxTurnRateDegrees.min, + SIMULATION_UGV_EXACT_VALUE_LIMITS.maxTurnRateDegrees.max, + "UGV maximum turn rate", + ), + invertSteering: booleanValue(record.invert_steering, "UGV steering inversion"), }; } function parseLayerViewerSettings( value: unknown, label: string, -): { inverted: boolean; axis: SimulationTransformAxis } { +): { rotationDegrees: SimulationEulerRotation } { const record = objectValue(value, label); - exactKeys(record, ["inverted", "axis"], label); + exactKeys(record, ["rotation_degrees"], label); + const rotation = objectValue(record.rotation_degrees, `${label} rotation`); + exactKeys(rotation, ["x", "y", "z"], `${label} rotation`); return { - inverted: booleanValue(record.inverted, `${label} inversion`), - axis: axisValue(record.axis), + rotationDegrees: { + x: rotationValue(rotation.x, `${label} X rotation`), + y: rotationValue(rotation.y, `${label} Y rotation`), + z: rotationValue(rotation.z, `${label} Z rotation`), + }, }; } @@ -466,9 +687,18 @@ function sceneTypeValue(value: unknown): SimulationSceneType { return invalid("scene type"); } -function axisValue(value: unknown): SimulationTransformAxis { - if (value === "x" || value === "y" || value === "z") return value; - return invalid("transform axis"); +function rotationValue(value: unknown, label: string): number { + if (typeof value === "number" && Number.isFinite(value) && value >= -360 && value <= 360) { + return value; + } + return invalid(label); +} + +function finiteNumberValue(value: unknown, minimum: number, maximum: number, label: string): number { + if (typeof value === "number" && Number.isFinite(value) && value >= minimum && value <= maximum) { + return value; + } + return invalid(label); } function qualityValue(value: unknown): SimulationViewerQuality { @@ -488,3 +718,7 @@ function statusValue(value: unknown): SimulationProjectStatus { function invalid(label: string): never { throw new Error(`Некорректный ${label}.`); } + +function clamp(value: number, minimum: number, maximum: number): number { + return Math.min(maximum, Math.max(minimum, value)); +} diff --git a/apps/control-station/src/core/simulation/sourceFiles.ts b/apps/control-station/src/core/simulation/sourceFiles.ts new file mode 100644 index 0000000..9a23b85 --- /dev/null +++ b/apps/control-station/src/core/simulation/sourceFiles.ts @@ -0,0 +1,146 @@ +import type { SimulationUploadCandidate } from "./projects"; + +const ARCHIVE_PATTERN = /\.(zip|rar|7z)$/i; +export const MAX_SIMULATION_SOURCE_BYTES = 16 * 1024 ** 3; + +interface LegacyFileEntry { + isFile: boolean; + isDirectory: boolean; + name: string; + fullPath: string; + file?: (callback: (file: File) => void, error?: (error: DOMException) => void) => void; + createReader?: () => LegacyDirectoryReader; +} + +interface LegacyDirectoryReader { + readEntries: ( + callback: (entries: LegacyFileEntry[]) => void, + error?: (error: DOMException) => void, + ) => void; +} + +export async function candidatesFromDrop( + transfer: DataTransfer, +): Promise { + const candidates: SimulationUploadCandidate[] = []; + for (const item of Array.from(transfer.items)) { + const entry = (item as DataTransferItem & { + webkitGetAsEntry?: () => LegacyFileEntry | null; + }).webkitGetAsEntry?.(); + if (entry?.isDirectory) { + await collectEntry(entry, candidates); + continue; + } + const file = item.getAsFile(); + if (file) { + candidates.push({ file, logicalPath: file.name }); + continue; + } + if (entry) await collectEntry(entry, candidates); + } + if (!candidates.length) { + for (const file of Array.from(transfer.files)) { + candidates.push({ file, logicalPath: file.name }); + } + } + return candidates; +} + +export async function archivesFromDrop(transfer: DataTransfer): Promise { + const candidates = await candidatesFromDrop(transfer); + const invalid = candidates.filter((candidate) => !ARCHIVE_PATTERN.test(candidate.logicalPath)); + if (invalid.length) { + throw new Error("В каталог можно перетащить только отдельные архивы ZIP, RAR или 7z."); + } + if (!candidates.length) { + throw new Error("В перетаскивании не найдено архивов ZIP, RAR или 7z."); + } + for (const candidate of candidates) { + if (candidate.logicalPath.includes("/")) { + throw new Error("Перетащите сами архивы, а не содержащую их папку."); + } + if (candidate.file.size <= 0) { + throw new Error(`Пустой архив: ${candidate.file.name}`); + } + if (candidate.file.size > MAX_SIMULATION_SOURCE_BYTES) { + throw new Error(`Архив ${candidate.file.name} превышает лимит 16 ГБ.`); + } + } + return candidates.map((candidate) => candidate.file); +} + +export function validateSimulationCandidates(candidates: SimulationUploadCandidate[]): void { + if (!candidates.length) throw new Error("Выберите архив или папку с результатом LCC/LCC2."); + const archives = candidates.filter((candidate) => ARCHIVE_PATTERN.test(candidate.logicalPath)); + if (archives.length) { + if (candidates.length !== 1) throw new Error("Архив нужно загружать одним файлом."); + if (archives[0]!.file.size <= 0) throw new Error(`Пустой файл: ${archives[0]!.logicalPath}`); + if (archives[0]!.file.size > MAX_SIMULATION_SOURCE_BYTES) { + throw new Error(`Архив ${archives[0]!.logicalPath} превышает лимит 16 ГБ.`); + } + return; + } + const descriptors = candidates.filter((candidate) => /\.lcc2?$/i.test(candidate.logicalPath)); + if (descriptors.length !== 1) { + throw new Error("В папке должна быть ровно одна сцена .lcc или .lcc2."); + } + const paths = new Set(); + let totalBytes = 0; + for (const candidate of candidates) { + const parts = candidate.logicalPath.split("/"); + if ( + !candidate.logicalPath + || candidate.logicalPath.startsWith("/") + || candidate.logicalPath.includes("\\") + || candidate.logicalPath.includes("\0") + || parts.some((part) => part === "" || part === "." || part === "..") + ) { + throw new Error("Папка содержит небезопасный путь."); + } + if (candidate.file.size <= 0) throw new Error(`Пустой файл: ${candidate.logicalPath}`); + totalBytes += candidate.file.size; + if (paths.has(candidate.logicalPath)) throw new Error(`Повторяющийся путь: ${candidate.logicalPath}`); + paths.add(candidate.logicalPath); + } + if (totalBytes > MAX_SIMULATION_SOURCE_BYTES) { + throw new Error("Папка с исходниками превышает лимит 16 ГБ."); + } +} + +export function hasDroppedFiles(transfer: DataTransfer): boolean { + return transfer.files.length > 0 + || Array.from(transfer.items).some((item) => item.kind === "file") + || Array.from(transfer.types).some((type) => type.toLowerCase() === "files"); +} + +export function archiveProjectName(fileName: string): string { + const name = fileName.replace(ARCHIVE_PATTERN, "").trim(); + return name || "Новая сцена"; +} + +function collectEntry( + entry: LegacyFileEntry, + target: SimulationUploadCandidate[], +): Promise { + if (entry.isFile && entry.file) { + return new Promise((resolve, reject) => entry.file?.(resolve, reject)) + .then((file) => { + target.push({ file, logicalPath: entry.fullPath.replace(/^\//, "") || file.name }); + }); + } + if (!entry.isDirectory || !entry.createReader) return Promise.resolve(); + return collectDirectory(entry.createReader(), target); +} + +async function collectDirectory( + reader: LegacyDirectoryReader, + target: SimulationUploadCandidate[], +): Promise { + while (true) { + const batch = await new Promise((resolve, reject) => { + reader.readEntries(resolve, reject); + }); + if (!batch.length) return; + for (const child of batch) await collectEntry(child, target); + } +} diff --git a/apps/control-station/src/productModel.ts b/apps/control-station/src/productModel.ts index ddf2af2..fe83942 100644 --- a/apps/control-station/src/productModel.ts +++ b/apps/control-station/src/productModel.ts @@ -161,7 +161,7 @@ export const workspaces: WorkspaceDefinition[] = [ label: "Симуляции", title: "Симуляции", eyebrow: "ТЕСТОВЫЙ КОНТУР / СИМУЛЯЦИИ", - description: "Каталог Gaussian-миров, переносимые сборки и интерактивные PlayCanvas-сцены.", + description: "Каталог Gaussian-миров, переносимые сборки и интерактивные сцены.", icon: "globe", kind: "simulations", groups: [], diff --git a/apps/control-station/src/styles/simulation.css b/apps/control-station/src/styles/simulation.css index e32b1ab..9e476ac 100644 --- a/apps/control-station/src/styles/simulation.css +++ b/apps/control-station/src/styles/simulation.css @@ -1,6 +1,8 @@ .simulation-workspace { display: grid; + height: 100%; min-width: 0; + grid-template-rows: auto minmax(0, 1fr); gap: 0.9rem; padding-bottom: 1rem; } @@ -17,6 +19,7 @@ .simulation-viewport__notice, .simulation-catalog__summary, .simulation-catalog__state, +.simulation-catalog__notice, .simulation-catalog__actions, .simulation-project-window__footer-state, .simulation-source-drop > div, @@ -60,7 +63,10 @@ } .simulation-catalog { + position: relative; display: grid; + min-height: 0; + align-content: start; gap: 0.55rem; min-width: 0; } @@ -100,23 +106,28 @@ .simulation-catalog__table-wrap { overflow: auto; border-radius: 1rem; - background: rgb(255 255 255 / 0.025); + background: transparent; } .simulation-catalog__table { width: 100%; - min-width: 62rem; - border-collapse: collapse; + min-width: 52rem; + border-collapse: separate; + border-spacing: 0 0.34rem; } .simulation-catalog__table th, .simulation-catalog__table td { - border-bottom: 1px solid var(--station-hairline); padding: 0.72rem 0.8rem; text-align: left; vertical-align: middle; } +.simulation-catalog__table th { + padding-top: 0.2rem; + padding-bottom: 0.18rem; +} + .simulation-catalog__table th:first-child, .simulation-catalog__table td:first-child { padding-left: 1rem; @@ -124,6 +135,8 @@ .simulation-catalog__table th:last-child, .simulation-catalog__table td:last-child { + width: 6.4rem; + min-width: 6.4rem; padding-right: 1rem; } @@ -131,12 +144,21 @@ transition: background var(--nodedc-duration-fast) var(--nodedc-ease-standard); } -.simulation-catalog__table tbody tr:hover { +.simulation-catalog__table tbody td { background: rgb(255 255 255 / 0.025); + transition: background var(--nodedc-duration-fast) var(--nodedc-ease-standard); } -.simulation-catalog__table tbody tr:last-child td { - border-bottom: 0; +.simulation-catalog__table tbody tr:hover td { + background: rgb(255 255 255 / 0.045); +} + +.simulation-catalog__table tbody td:first-child { + border-radius: 0.85rem 0 0 0.85rem; +} + +.simulation-catalog__table tbody td:last-child { + border-radius: 0 0.85rem 0.85rem 0; } .simulation-catalog__table td { @@ -156,7 +178,7 @@ display: flex; align-items: center; gap: 0.65rem; - min-width: 14rem; + min-width: 11rem; border: 0; background: transparent; color: inherit; @@ -165,16 +187,26 @@ cursor: pointer; } +.simulation-catalog__table td:first-child > button > span:last-child { + min-width: 0; +} + .simulation-catalog__table td:first-child > button strong { display: block; max-width: 20rem; - overflow: hidden; + overflow: clip; color: var(--nodedc-text-primary); font-size: 0.7rem; text-overflow: ellipsis; white-space: nowrap; } +.simulation-catalog__table td:first-child > button small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .simulation-catalog__project-icon, .simulation-catalog__empty > span { display: grid; @@ -188,10 +220,53 @@ } .simulation-catalog__actions { + display: flex; justify-content: flex-end; gap: 0.28rem; } +.simulation-catalog__progress { + display: grid; + min-width: 0; + gap: 0.26rem; +} + +.simulation-catalog__progress-meta, +.simulation-catalog__progress-stage { + overflow: hidden; + color: var(--nodedc-text-muted); + font-size: 0.54rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.simulation-catalog__progress-track { + position: relative; + overflow: hidden; + width: 100%; + height: 0.3rem; + border-radius: 999px; + background: rgb(255 255 255 / 0.08); +} + +.simulation-catalog__progress-track i { + position: absolute; + inset: 0 auto 0 0; + min-width: 0; + border-radius: inherit; + background: rgb(var(--nodedc-accent-rgb)); + transition: width var(--nodedc-duration-fast) var(--nodedc-ease-standard); +} + +.simulation-catalog__progress[data-indeterminate="true"] .simulation-catalog__progress-track i { + width: 28% !important; + animation: simulation-progress-pulse 1.6s ease-in-out infinite; +} + +.simulation-catalog__progress-stage { + font-size: 0.52rem; +} + .simulation-catalog__state, .simulation-catalog__empty { min-height: 20rem; @@ -203,6 +278,67 @@ font-size: 0.65rem; } +.simulation-catalog__notice { + justify-content: flex-start; + gap: 0.55rem; + border-radius: 0.85rem; + background: rgb(var(--nodedc-danger-rgb) / 0.08); + color: var(--nodedc-danger); + padding: 0.7rem 0.85rem; + font-size: 0.62rem; +} + +.simulation-catalog__drop-overlay { + position: absolute; + z-index: 4; + inset: 0; + display: grid; + place-content: center; + justify-items: center; + gap: 0.55rem; + border-radius: 1rem; + background: var(--nodedc-canvas); + color: var(--nodedc-text-muted); + text-align: center; +} + +.simulation-catalog__drop-mark { + display: grid; + width: 4rem; + height: 4rem; + margin-bottom: 0.25rem; + place-items: center; + border-radius: 50%; + background: var(--nodedc-glass-control-active); + color: var(--nodedc-glass-control-active-text); +} + +.simulation-catalog__drop-overlay strong { + color: var(--nodedc-text-primary); + font-size: 0.86rem; +} + +.simulation-catalog__drop-overlay span { + font-size: 0.64rem; +} + +.simulation-catalog__drop-overlay small { + color: var(--nodedc-text-muted); + font-size: 0.56rem; +} + +@keyframes simulation-progress-pulse { + from { transform: translateX(-105%); } + to { transform: translateX(360%); } +} + +@media (prefers-reduced-motion: reduce) { + .simulation-catalog__progress[data-indeterminate="true"] .simulation-catalog__progress-track i { + animation: none; + transform: translateX(0); + } +} + .simulation-catalog__state > div { display: grid; gap: 0.2rem; @@ -342,6 +478,9 @@ } .simulation-viewport__toolbar { + position: sticky; + z-index: 10; + top: 0; display: grid; grid-template-columns: minmax(12rem, 1fr) auto minmax(12rem, 1fr); align-items: center; @@ -379,19 +518,34 @@ top: calc(100% + 0.65rem); right: 0; display: grid; + grid-template-rows: auto minmax(0, 1fr); width: min(21rem, calc(100cqw - 1.3rem)); - gap: 0.75rem; - border-radius: 0.9rem; - background: rgb(25 27 31 / 0.98); - box-shadow: 0 1rem 3rem rgb(0 0 0 / 0.42); - padding: 0.8rem; + max-height: min(44rem, calc(100vh - 7rem)); + overflow: clip; + gap: 0; + padding: 0; } .simulation-viewport__settings-head { + position: relative; + z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; + background: var(--nodedc-glass-panel-bg-strong); + padding: 0.8rem; + box-shadow: 0 0.55rem 1rem rgb(0 0 0 / 0.18); + backdrop-filter: blur(18px); +} + +.simulation-viewport__settings-scroll { + display: grid; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + gap: 0.75rem; + padding: 0.75rem 0.8rem 0.8rem; } .simulation-viewport__settings-head > div, @@ -440,16 +594,49 @@ gap: 0.55rem; } -.simulation-viewport__settings-layer-row { +.simulation-viewport__settings-rotations { display: grid; - grid-template-columns: minmax(0, 1fr) 4.5rem; - align-items: center; - gap: 0.55rem; + gap: 0.7rem; } -.simulation-viewport__axis-select { - width: 4.5rem; - min-width: 0; +.simulation-viewport__rotation-row { + display: grid; + gap: 0.4rem; +} + +.simulation-viewport__rotation-row > strong { + color: var(--nodedc-text-secondary); + font-size: 0.6rem; +} + +.simulation-viewport__rotation-fields { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.45rem; +} + +.simulation-viewport__rotation-field { + gap: 0.22rem; +} + +.simulation-viewport__rotation-field .nodedc-field__label-row { + padding-inline: 0.15rem; +} + +.simulation-viewport__rotation-field .nodedc-field__label { + font-size: 0.54rem; +} + +.simulation-viewport__rotation-input { + min-height: 1.75rem; + padding: 0.2rem 0.55rem; + font-size: 0.64rem; + text-align: right; +} + +.simulation-viewport__rotation-input:disabled { + cursor: not-allowed; + opacity: 0.45; } .simulation-viewport__settings-error { @@ -459,6 +646,30 @@ padding: 0.55rem 0.65rem; } +.simulation-ugv-settings { + gap: 0.7rem; +} + +.simulation-ugv-settings__subhead { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.65rem; + margin-top: 0.1rem; + padding-top: 0.55rem; + border-top: 1px solid rgb(255 255 255 / 0.055); +} + +.simulation-ugv-settings__subhead strong { + color: var(--nodedc-text-secondary); + font-size: 0.58rem; +} + +.simulation-ugv-settings__save { + width: 100%; + margin-top: 0.15rem; +} + .simulation-viewport__toolbar-balance { min-width: 0; } @@ -472,7 +683,7 @@ .simulation-viewport__stage { position: relative; min-width: 0; - min-height: 30rem; + min-height: 18rem; overflow: hidden; } @@ -491,6 +702,16 @@ cursor: grabbing; } +.simulation-viewport[data-control-mode="ugv"] .simulation-viewport__stage canvas, +.simulation-viewport[data-control-mode="ugv"] .simulation-viewport__stage canvas:active { + cursor: grab; +} + +.simulation-viewport[data-control-mode="ugv"] .simulation-viewport__stage canvas.is-orbiting, +.simulation-viewport[data-control-mode="ugv"] .simulation-viewport__stage canvas.is-orbiting:active { + cursor: grabbing; +} + .simulation-viewport__stage canvas:focus-visible { box-shadow: inset 0 0 0 2px rgb(var(--nodedc-accent-rgb) / 0.7); } diff --git a/apps/control-station/src/workspaces/simulation/SimulationWorkspace.tsx b/apps/control-station/src/workspaces/simulation/SimulationWorkspace.tsx index c5eccd1..50deeb5 100644 --- a/apps/control-station/src/workspaces/simulation/SimulationWorkspace.tsx +++ b/apps/control-station/src/workspaces/simulation/SimulationWorkspace.tsx @@ -1,25 +1,45 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type DragEvent, +} from "react"; import { ActivityIndicator, Button, ConfirmationModal, GlassSurface, Icon, - IconButton, StatusBadge, } from "@nodedc/ui-react"; +import { SimulationCatalog } from "../../components/simulation/SimulationCatalog"; import { SimulationProjectWindow } from "../../components/simulation/SimulationProjectWindow"; import { SimulationViewport } from "../../components/simulation/SimulationViewport"; import { + createSimulationProject, deleteSimulationProject, fetchSimulationProjects, retrySimulationProject, + uploadSimulationProjectSource, type SimulationProject, type SimulationProjectStatus, + type SimulationUploadProgress, } from "../../core/simulation/projects"; +import { + archiveProjectName, + archivesFromDrop, + hasDroppedFiles, +} from "../../core/simulation/sourceFiles"; -const ACTIVE_STATUSES = new Set(["queued", "processing", "importing"]); +const BUILD_STATUSES = new Set(["queued", "processing", "importing"]); + +interface BrowserUpload { + project: SimulationProject; + file: File; +} export function SimulationWorkspace() { const [projects, setProjects] = useState([]); @@ -30,6 +50,15 @@ export function SimulationWorkspace() { const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); const [retryingId, setRetryingId] = useState(null); + const [pendingUploads, setPendingUploads] = useState([]); + const [uploadProgress, setUploadProgress] = useState>( + () => new Map(), + ); + const [queueError, setQueueError] = useState(null); + const [dropping, setDropping] = useState(false); + const activeUploadRef = useRef<{ projectId: string; controller: AbortController } | null>(null); + const intakeChainRef = useRef>(Promise.resolve()); + const dragDepthRef = useRef(0); const load = useCallback(async (signal?: AbortSignal) => { try { @@ -51,11 +80,60 @@ export function SimulationWorkspace() { }, [load]); useEffect(() => { - if (!projects.some((project) => ACTIVE_STATUSES.has(project.status))) return; + if (!projects.some((project) => BUILD_STATUSES.has(project.status))) return; const timer = window.setInterval(() => void load(), 2_000); return () => window.clearInterval(timer); }, [load, projects]); + useEffect(() => { + const next = pendingUploads[0]; + if (!next || activeUploadRef.current) return; + const controller = new AbortController(); + activeUploadRef.current = { projectId: next.project.projectId, controller }; + void uploadSimulationProjectSource( + next.project, + [{ file: next.file, logicalPath: next.file.name }], + (progress) => { + setUploadProgress((current) => { + const updated = new Map(current); + updated.set(next.project.projectId, progress); + return updated; + }); + setProjects((current) => current.map((project) => ( + project.projectId === next.project.projectId + ? { + ...project, + source: { ...project.source, uploadedByteLength: progress.uploadedBytes }, + } + : project + ))); + }, + controller.signal, + ).then((queued) => { + setProjects((current) => current.map((project) => ( + project.projectId === queued.projectId ? queued : project + ))); + setQueueError(null); + }).catch((caught) => { + if (controller.signal.aborted) return; + setQueueError(caught instanceof Error + ? `${next.file.name}: ${caught.message}` + : `${next.file.name}: загрузка остановлена.`); + }).finally(() => { + setPendingUploads((current) => current.filter( + (item) => item.project.projectId !== next.project.projectId, + )); + setUploadProgress((current) => { + const updated = new Map(current); + updated.delete(next.project.projectId); + return updated; + }); + activeUploadRef.current = null; + }); + }, [pendingUploads]); + + useEffect(() => () => activeUploadRef.current?.controller.abort(), []); + const selected = useMemo( () => projects.find((project) => project.projectId === selectedId) ?? null, [projects, selectedId], @@ -72,8 +150,9 @@ export function SimulationWorkspace() { }; const acceptSaved = (project: SimulationProject) => { + const wasEditing = editing !== null; setProjects((current) => [project, ...current.filter((item) => item.projectId !== project.projectId)]); - setSelectedId(project.projectId); + if (!wasEditing) setSelectedId(project.projectId); setWindowOpen(false); setEditing(null); }; @@ -86,10 +165,77 @@ export function SimulationWorkspace() { const confirmDelete = async () => { if (!deleting) return; - await deleteSimulationProject(deleting.projectId); - setProjects((current) => current.filter((project) => project.projectId !== deleting.projectId)); - if (selectedId === deleting.projectId) setSelectedId(null); - setDeleting(null); + const pending = pendingUploads.find((item) => item.project.projectId === deleting.projectId); + if (activeUploadRef.current?.projectId === deleting.projectId) { + activeUploadRef.current.controller.abort(); + } + setPendingUploads((current) => current.filter( + (item) => item.project.projectId !== deleting.projectId, + )); + try { + await deleteSimulationProject(deleting.projectId); + setProjects((current) => current.filter((project) => project.projectId !== deleting.projectId)); + setUploadProgress((current) => { + const updated = new Map(current); + updated.delete(deleting.projectId); + return updated; + }); + if (selectedId === deleting.projectId) setSelectedId(null); + setDeleting(null); + } catch (caught) { + if (pending) setPendingUploads((current) => [pending, ...current]); + const message = caught instanceof Error ? caught.message : "Не удалось удалить проект."; + setQueueError(message); + throw caught; + } + }; + + const enqueueDroppedArchives = useCallback((files: File[]) => { + intakeChainRef.current = intakeChainRef.current.then(async () => { + for (const file of files) { + try { + const project = await createSimulationProject( + archiveProjectName(file.name), + "outdoor", + [{ file, logicalPath: file.name }], + ); + setProjects((current) => [ + project, + ...current.filter((item) => item.projectId !== project.projectId), + ]); + setPendingUploads((current) => [...current, { project, file }]); + } catch (caught) { + setQueueError(caught instanceof Error + ? `${file.name}: ${caught.message}` + : `${file.name}: не удалось создать проект.`); + } + } + }); + }, []); + + const handleDrop = async (transfer: DataTransfer) => { + dragDepthRef.current = 0; + setDropping(false); + setQueueError(null); + try { + enqueueDroppedArchives(await archivesFromDrop(transfer)); + } catch (caught) { + setQueueError(caught instanceof Error ? caught.message : "Архивы не приняты."); + } + }; + + const handleDragEnter = (event: DragEvent) => { + if (!hasDroppedFiles(event.dataTransfer)) return; + event.preventDefault(); + dragDepthRef.current += 1; + setDropping(true); + }; + + const handleDragLeave = (event: DragEvent) => { + if (!hasDroppedFiles(event.dataTransfer)) return; + event.preventDefault(); + dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); + if (dragDepthRef.current === 0) setDropping(false); }; const retry = async (project: SimulationProject) => { @@ -141,7 +287,7 @@ export function SimulationWorkspace() {
- {selected.provider.state ?? selected.status} + {providerStateLabel(selected.provider.state, selected.status)} {selected.status === "failed" ? ( -
-
-
Проектов{projects.length.toLocaleString("ru-RU")}
-
Готово{projects.filter((project) => project.status === "ready").length}
-
В работе{projects.filter((project) => ACTIVE_STATUSES.has(project.status)).length}
-
Исходники{formatBytes(projects.reduce((sum, project) => sum + project.source.totalByteLength, 0))}
-
- {loading ? ( -
- - Читаем каталог сцен -
- ) : error ? ( -
- -
Каталог недоступен{error}
- -
- ) : projects.length === 0 ? ( -
- - Проектов пока нет -

Создайте первый мир из папки LCC/LCC2 или архива ZIP, RAR, 7z.

- -
- ) : ( -
- - - - - - - - - - - - - {projects.map((project) => { - const status = statusPresentation(project.status); - return ( - - - - - - - - - - ); - })} - -
ПроектИсточникРазмерСплатыСтатусОбновлён -
- - {project.source.kind === "archive" ? "Архив" : "Папка"}{project.source.files.length} файл(ов){formatBytes(project.source.totalByteLength)}{status.label}{formatDate(project.updatedAtUtc)} -
- setDeleting(project)}> - - - openEdit(project)}> - - -
-
-
- )} -
+ void load()} + onSelect={(project) => setSelectedId(project.projectId)} + onEdit={openEdit} + onDelete={setDeleting} + /> { - const [productModel, workspaceHub, workspace, styles] = await Promise.all([ + const [productModel, workspaceHub, workspace, catalog, styles] = await Promise.all([ read("productModel.ts"), read("workspaces/Workspaces.tsx"), read("workspaces/simulation/SimulationWorkspace.tsx"), + read("components/simulation/SimulationCatalog.tsx"), read("styles.css"), ]); assert.match(productModel, /id: "simulations"[\s\S]*root: "polygon"[\s\S]*kind: "simulations"/); assert.match(workspaceHub, /case "simulations":[\s\S]*/); - assert.match(workspace, /simulation-catalog__table/); + assert.match(catalog, /simulation-catalog__table/); + assert.match(workspace, /archivesFromDrop/); + assert.match(workspace, /uploadSimulationProjectSource/); + assert.match(catalog, /colSpan=\{3\}/); + assert.match(catalog, /Ожидает обработки/); + assert.match(catalog, /Прогресс \$\{roundedPercent\}% \|/); + assert.match(catalog, /Осталось примерно \$\{minutes\} мин/); + assert.match(catalog, /simulation-catalog__progress-track/); + assert.doesNotMatch(catalog, /disabled=\{ACTIVE_STATUSES\.has/); assert.match(workspace, / { - const [core, window] = await Promise.all([ + const [core, window, sourceFiles] = await Promise.all([ read("core/simulation/projects.ts"), read("components/simulation/SimulationProjectWindow.tsx"), + read("core/simulation/sourceFiles.ts"), ]); assert.match(core, /const API_ROOT = "\/api\/v1\/simulation-worlds\/projects"/); assert.match(core, /method: "HEAD"/); assert.match(core, /method: "PATCH"/); assert.match(core, /"Upload-Offset"/); + assert.match(core, /estimatedSecondsRemaining/); + assert.match(core, /smoothedBytesPerSecond \* 0\.7 \+ currentBytesPerSecond \* 0\.3/); + assert.match(core, /estimateSimulationProcessing/); + assert.match(core, /jobCreatedAtUtc/); + assert.match(core, /stateStartedAtUtc/); + assert.match(core, /REFERENCE_STREAM_SECONDS = 2_317/); assert.doesNotMatch(core, /Authorization|Bearer|18090|Worker 006/); assert.match(window, /accept="\.zip,\.rar,\.7z"/); assert.match(window, /webkitdirectory/); - assert.match(window, /webkitGetAsEntry/); - assert.match(window, /ровно одна сцена \.lcc или \.lcc2/); + assert.match(sourceFiles, /webkitGetAsEntry/); + assert.match(sourceFiles, /entry\?\.isDirectory/); + assert.match(sourceFiles, /const file = item\.getAsFile\(\)/); + assert.match(sourceFiles, /MAX_SIMULATION_SOURCE_BYTES = 16 \* 1024 \*\* 3/); + assert.match(sourceFiles, /item\.kind === "file"/); + assert.match(sourceFiles, /ровно одна сцена \.lcc или \.lcc2/); + assert.match(sourceFiles, /отдельные архивы ZIP, RAR или 7z/); assert.doesNotMatch(window, /Загружаем источник/); + assert.match(window, /считаем время/); }); test("failed simulation can restart from its retained Mission Core source", async () => { @@ -54,9 +78,13 @@ test("failed simulation can restart from its retained Mission Core source", asyn }); test("PlayCanvas owns the realtime scene graph without an iframe or React entity tree", async () => { - const [runtime, viewport, packageDocument] = await Promise.all([ + const [runtime, ugv, viewport, ugvSettings, projects, styles, packageDocument] = await Promise.all([ read("components/simulation/PlayCanvasRuntime.ts"), + read("components/simulation/SimulationUgvController.ts"), read("components/simulation/SimulationViewport.tsx"), + read("components/simulation/SimulationUgvSettingsPanel.tsx"), + read("core/simulation/projects.ts"), + read("styles/simulation.css"), readFile(new URL("../package.json", import.meta.url), "utf8"), ]); @@ -72,41 +100,118 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity assert.match(runtime, /maximum: \{[^}]*lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2, splatBudget: 4_000_000, targetFps: 60/); assert.match(runtime, /app\.scene\.gsplat\.splatBudget = profile\.splatBudget/); assert.match(runtime, /app\.autoRender = false/); + assert.match(runtime, /app\.on\("update", this\.updateFrame\)/); + assert.match(runtime, /if \(!cameraChanged && this\.controlMode === "free"\) return/); + assert.doesNotMatch(runtime, /app\.on\("frameupdate"/); assert.match(runtime, /WEBGL_lose_context/); assert.match(runtime, /selected\.horizontal[^\n]+frame\.mouse\[0\] \*= -1/); assert.match(runtime, /selected\.vertical[^\n]+frame\.mouse\[1\] \*= -1/); assert.match(runtime, /setCameraInversion/); - assert.match(runtime, /async setViewMode\([\s\S]*this\.resetCameraInteraction\(\);[\s\S]*this\.viewMode = mode/); - assert.match(runtime, /input\._pointerId = -1/); + assert.match(runtime, /async setViewMode\([\s\S]*this\.viewMode = mode/); + assert.doesNotMatch(runtime.match(/async setViewMode\([\s\S]*?\n }/)?.[0] ?? "", /resetCameraInteraction/); + assert.match(runtime, /controller\.fire\("state"\)/); assert.match(runtime, /state\.mouse\.fill\(0\)/); assert.match(runtime, /new Asset\([\s\S]*"container"/); - assert.match(runtime, /instantiateRenderEntity/); + assert.match(runtime, /instantiateModelEntity/); assert.match(runtime, /applyWorldTransform/); assert.match(runtime, /setLayerWorldTransform/); - assert.match(runtime, /PLAYCANVAS_X_180_TRANSFORM/); - assert.match(runtime, /PLAYCANVAS_Y_180_TRANSFORM/); - assert.match(runtime, /PLAYCANVAS_Z_180_TRANSFORM/); + assert.match(runtime, /playCanvasEulerTransform/); + assert.match(runtime, /setFromEulerAngles/); assert.match(runtime, /DracoDecoderModule/); assert.match(runtime, /draco_decoder_gltf\.wasm/); assert.match(runtime, /ensureCollision/); + assert.match(runtime, /setControlMode/); + assert.match(runtime, /setUgvPreset/); + assert.match(runtime, /SimulationUgvController/); + assert.match(runtime, /for \(const model of this\.collisionVisuals\) model\.enabled = visible/); + assert.match(runtime, /instantiateModelEntity/); + assert.match(runtime, /this\.ugvController\?\.disable\(\)/); + assert.match(runtime, /this\.collisionMaterial\.blendType = combined \? BLEND_NORMAL : BLEND_NONE/); + assert.match(runtime, /private requestRender\(\): void/); assert.match(runtime, /app\.root\.addChild/); assert.match(runtime, /dispose\(\)/); assert.doesNotMatch(`${runtime}\n${viewport}`, /iframe|\(project\.viewerSettings\.quality\)/); assert.match(viewport, /runtimeRef\.current\?\.home\(\)/); assert.match(viewport, /Настройки сцены/); - assert.match(viewport, /Инверсия визуального слоя/); - assert.match(viewport, /Инверсия collision-слоя/); + assert.match(viewport, /Гауссовый слой/); + assert.match(viewport, /Слой коллизий/); assert.match(viewport, /Инверсия управления по горизонтали/); assert.match(viewport, /Инверсия управления по вертикали/); - assert.match(viewport, /Ось инверсии визуального слоя/); - assert.match(viewport, /Ось инверсии collision-слоя/); + assert.match(viewport, /ROTATION_AXES/); + assert.match(viewport, /PlayCanvas|PlayCanvas загружает|Streamed SOG/); + assert.match(ugv, /AMMO_MODULE_ROOT = "\/vendor\/playcanvas\/ammo\/2026-08-25"/); + assert.match(ugv, /new this\.ammo\.btRaycastVehicle/); + assert.match(ugv, /type: "mesh"/); + assert.match(ugv, /MeshoptSimplifier\.simplify/); + assert.match(ugv, /PHYSICS_PROXY_MAX_TRIANGLES = 240_000/); + assert.match(ugv, /createPhysicsProxyMesh/); + assert.match(ugv, /findSafeSpawnPosition/); + assert.match(ugv, /physics\.raycastFirst/); + assert.match(ugv, /runCleanup\("destroy static physics mesh", \(\) => mesh\.destroy\(\)\)/); + assert.match(ugv, /type: "static"/); + assert.match(ugv, /mass: this\.settings\.massKg/); + assert.match(ugv, /suspensionRestLength/); + assert.match(ugv, /requestedTurn = Number\(this\.isPressed\("KeyA", "ArrowLeft"\)\) - Number\(this\.isPressed\("KeyD", "ArrowRight"\)\)/); + assert.match(ugv, /leftCommand = clamp\(forwardCommand - turnCommand/); + assert.match(ugv, /rightCommand = clamp\(forwardCommand \+ turnCommand/); + assert.match(ugv, /this\.settings\.invertSteering/); + assert.match(ugv, /this\.settings\.maxSpeedMetersPerSecond/); + assert.match(ugv, /this\.settings\.maxTurnRateDegrees/); + assert.match(ugv, /desiredSpeed = forwardInput \* maxSpeed/); + assert.match(ugv, /maximumAcceleration = clamp\(1\.4 \+ maxSpeed \* 0\.35, 1\.8, 4\.2\)/); + assert.match(ugv, /SERVICE_BRAKE_DECELERATION_MPS2 = 1\.8/); + assert.match(ugv, /horizontalSpeed - deceleration \* Math\.max\(0, deltaSeconds\)/); + assert.match(ugv, /this\.vehicle\.setBrake\(0, index\)/); + assert.doesNotMatch(ugv, /massKg \* 3/); + assert.match(ugv, /pureTurn = !braking && forwardInput === 0 && turnInput !== 0/); + assert.match(ugv, /desiredYawRate = -turnInput \* maxTurnRate/); + assert.match(ugv, /this\.orbitYaw = \(this\.orbitYaw - deltaX \* 0\.006\)/); + assert.match(ugv, /CAMERA_RETURN_DELAY_SECONDS = 1\.2/); + assert.match(ugv, /CAMERA_RETURN_DURATION_SECONDS = 2/); + assert.match(ugv, /approachAngle\(this\.orbitYaw, rearOrbitYaw/); + assert.match(ugv, /this\.canvas\.addEventListener\("pointerdown", this\.onOrbitPointerDown\)/); + assert.match(ugv, /this\.canvas\.addEventListener\("wheel", this\.onOrbitWheel/); + assert.match(ugv, /vehicleGeometryChanged/); + assert.match(ugv, /removeAction\(this\.vehicle as NativeObject\)/); + assert.match(ugv, /runCleanup\("destroy static physics proxy", \(\) => entity\.destroy\(\)\)/); + assert.match(ugv, /runCleanup\("destroy vehicle entity"/); + assert.match(ugvSettings, /Физика UGV/); + assert.match(ugvSettings, /Дифференциальный привод/); + assert.match(ugvSettings, /Масса конструкции/); + assert.match(ugvSettings, /Общая высота/); + assert.match(ugvSettings, /Клиренс/); + assert.match(ugvSettings, /Скорость движения/); + assert.match(ugvSettings, /Скорость разворота/); + assert.match(ugvSettings, /exactValueBounds=\{SIMULATION_UGV_EXACT_VALUE_LIMITS\.maxSpeedMetersPerSecond\}/); + assert.match(projects, /maxSpeedMetersPerSecond: \{ min: 0, max: 1_000_000_000 \}/); + assert.match(ugvSettings, /Инверсия управления A\/D/); + assert.match(ugvSettings, /Сохранить пресет/); + assert.match(ugvSettings, / None: self.root = data_dir.expanduser().resolve() / "simulation-worlds" @@ -105,13 +114,15 @@ class SimulationProjectStore: raise SimulationProjectError("simulation source paths must be unique") logical_paths.add(logical_path) total_bytes += byte_length - normalized.append({ - "file_id": f"src-{index:05d}-{uuid4().hex[:8]}", - "logical_path": logical_path, - "byte_length": byte_length, - "uploaded_bytes": 0, - "sha256": None, - }) + normalized.append( + { + "file_id": f"src-{index:05d}-{uuid4().hex[:8]}", + "logical_path": logical_path, + "byte_length": byte_length, + "uploaded_bytes": 0, + "sha256": None, + } + ) if total_bytes > MAX_SOURCE_BYTES: raise SimulationProjectError("simulation source exceeds byte admission") if source_kind == "archive": @@ -141,7 +152,9 @@ class SimulationProjectStore: "provider": { "provider_id": "gaussian-pipeline", "job_id": None, + "job_created_at_utc": None, "state": None, + "state_started_at_utc": None, "progress": None, "runtime": None, }, @@ -269,7 +282,9 @@ class SimulationProjectStore: document["provider"] = { "provider_id": "gaussian-pipeline", "job_id": None, + "job_created_at_utc": None, "state": None, + "state_started_at_utc": None, "progress": None, "runtime": None, } @@ -286,7 +301,9 @@ class SimulationProjectStore: *, status: str, provider_job_id: str | None = None, + provider_job_created_at_utc: str | None = None, provider_state: str | None = None, + provider_state_started_at_utc: str | None = None, progress: object = None, bundle_sha256: str | None = None, ) -> dict[str, Any]: @@ -299,8 +316,26 @@ class SimulationProjectStore: if PROVIDER_JOB_ID_PATTERN.fullmatch(provider_job_id) is None: raise SimulationProjectError("simulation provider job id is invalid") document["provider"]["job_id"] = provider_job_id + if provider_job_created_at_utc is not None: + document["provider"]["job_created_at_utc"] = _provider_timestamp( + provider_job_created_at_utc, + "job creation", + ) if provider_state is not None: + previous_state = document["provider"].get("state") document["provider"]["state"] = provider_state + if ( + previous_state != provider_state + or document["provider"].get("state_started_at_utc") is None + ): + document["provider"]["state_started_at_utc"] = ( + _provider_timestamp( + provider_state_started_at_utc, + "state start", + ) + if provider_state_started_at_utc is not None + else utc_now_iso() + ) if progress is not None: document["provider"]["progress"] = progress if bundle_sha256 is not None: @@ -362,13 +397,15 @@ class SimulationProjectStore: raise SimulationProjectNotFoundError("simulation artifact is unavailable") return target, descriptor - def delete(self, project_id: str) -> None: + def delete(self, project_id: str, *, allow_active: bool = False) -> None: with self._lock: document = self._read(project_id) - if document["status"] not in TERMINAL_STATES and document["status"] != "uploading": - raise SimulationProjectConflictError( - "active simulation project cannot be deleted" - ) + if ( + not allow_active + and document["status"] not in TERMINAL_STATES + and document["status"] != "uploading" + ): + raise SimulationProjectConflictError("active simulation project cannot be deleted") shutil.rmtree(self._project_root(project_id)) def source_root(self, project_id: str) -> Path: @@ -391,7 +428,11 @@ class SimulationProjectStore: or document.get("project_id") != project_id ): raise SimulationProjectError("persisted simulation project identity is invalid") - document.setdefault("viewer_settings", _default_viewer_settings()) + document["viewer_settings"] = _migrate_viewer_settings(document.get("viewer_settings")) + provider = document.get("provider") + if isinstance(provider, dict): + provider.setdefault("job_created_at_utc", None) + provider.setdefault("state_started_at_utc", None) return document def _write(self, document: dict[str, Any]) -> None: @@ -425,21 +466,67 @@ class SimulationProjectService: ) -> None: self.store = store self.provider_factory = provider_factory + self._condition = threading.Condition() + self._queue: deque[str] = deque() + self._queued_ids: set[str] = set() + self._active_project_id: str | None = None + self._worker: threading.Thread | None = None + self._cancel_events: dict[str, threading.Event] = {} + self._inflight_jobs: dict[str, str] = {} def recover_pending(self) -> int: - pending = [ - project for project in self.store.list() - if project.get("status") in ACTIVE_STATES - ] + pending = sorted( + [project for project in self.store.list() if project.get("status") in ACTIVE_STATES], + key=lambda project: ( + str(project.get("created_at_utc")), + str(project.get("project_id")), + ), + ) for project in pending: - threading.Thread( - target=self.process, - args=(str(project["project_id"]),), - name=f"simulation-recovery-{str(project['project_id'])[-8:]}", - daemon=True, - ).start() + self.enqueue(str(project["project_id"])) return len(pending) + def enqueue(self, project_id: str) -> None: + project = self.store.get(project_id) + if project.get("status") not in ACTIVE_STATES: + raise SimulationProjectConflictError( + "simulation project is not ready for queued processing" + ) + with self._condition: + if project_id in self._queued_ids or project_id == self._active_project_id: + return + event = self._cancel_events.setdefault(project_id, threading.Event()) + event.clear() + self._queue.append(project_id) + self._queued_ids.add(project_id) + if self._worker is None or not self._worker.is_alive(): + self._worker = threading.Thread( + target=self._run_queue, + name="simulation-build-queue", + daemon=True, + ) + self._worker.start() + self._condition.notify_all() + + def _run_queue(self) -> None: + while True: + with self._condition: + if not self._queue: + self._worker = None + self._condition.notify_all() + return + project_id = self._queue.popleft() + self._queued_ids.discard(project_id) + self._active_project_id = project_id + try: + self.process(project_id) + finally: + with self._condition: + self._active_project_id = None + self._inflight_jobs.pop(project_id, None) + self._cancel_events.pop(project_id, None) + self._condition.notify_all() + def begin_build(self, project_id: str) -> dict[str, Any]: project = self.store.get(project_id) if project.get("status") == "failed": @@ -460,15 +547,20 @@ class SimulationProjectService: finally: provider.close() current_state = job.get("state") - if ( - isinstance(current_state, str) - and current_state in PROVIDER_JOB_STATES - {"failed"} - ): + if isinstance(current_state, str) and current_state in PROVIDER_JOB_STATES - { + "failed" + }: return self.store.update_processing( project_id, status="processing", provider_job_id=job_id, + provider_job_created_at_utc=_optional_provider_timestamp( + job.get("created_at_utc") + ), provider_state=current_state, + provider_state_started_at_utc=_optional_provider_timestamp( + job.get("updated_at_utc") + ), progress=job.get("progress"), ) if project.get("status") in {"failed", "ready"}: @@ -488,14 +580,17 @@ class SimulationProjectService: def process(self, project_id: str) -> None: provider: GaussianPipelineGateway | None = None try: + self._raise_if_cancelled(project_id) project = self.store.get(project_id) provider = self.provider_factory() if provider is None: raise SimulationProjectError("Gaussian Pipeline не настроен.") _retry_provider_unavailable(provider.capabilities) + self._raise_if_cancelled(project_id) existing_job_id = project["provider"].get("job_id") if isinstance(existing_job_id, str): job_id = existing_job_id + self._register_inflight_job(project_id, job_id, provider) else: if project["status"] != "queued": raise SimulationProjectError( @@ -522,25 +617,33 @@ class SimulationProjectService: "outputs": { "preview_sog": True, "streamed_sog": True, - "collision": True, + "collision": False, }, "preview_lod": "coarsest", - "collision_profile": _collision_profile(str(project["scene_type"])), + "collision_profile": None, } submitted = provider.submit_build(request) job_id = submitted.get("job_id") if not isinstance(job_id, str): raise SimulationProjectError("Gaussian Pipeline не вернул job id.") + self._register_inflight_job(project_id, job_id, provider) self.store.update_processing( project_id, status="processing", provider_job_id=job_id, + provider_job_created_at_utc=_optional_provider_timestamp( + submitted.get("created_at_utc") + ), provider_state=str(submitted.get("state") or "queued"), + provider_state_started_at_utc=_optional_provider_timestamp( + submitted.get("updated_at_utc") + ), progress=submitted.get("progress"), bundle_sha256=source.bundle_sha256, ) deadline = time.monotonic() + PROVIDER_POLL_TIMEOUT_SECONDS while True: + self._raise_if_cancelled(project_id) if time.monotonic() >= deadline: raise SimulationProjectError( "Gaussian Pipeline превысил лимит ожидания сборки." @@ -554,7 +657,13 @@ class SimulationProjectService: self.store.update_processing( project_id, status="processing", + provider_job_created_at_utc=_optional_provider_timestamp( + job.get("created_at_utc") + ), provider_state=str(state or "unknown"), + provider_state_started_at_utc=_optional_provider_timestamp( + job.get("updated_at_utc") + ), progress=job.get("progress"), ) if state == "failed": @@ -572,9 +681,11 @@ class SimulationProjectService: provider_state="ready", ) result = _retry_provider_unavailable(lambda: provider.get_result(job_id)) + self._raise_if_cancelled(project_id) artifacts = _artifact_descriptors(result.get("artifacts")) artifacts_root = self.store.artifacts_root(project_id) for descriptor in artifacts: + self._raise_if_cancelled(project_id) _retry_provider_unavailable( lambda descriptor=descriptor: provider.download_artifact( job_id, @@ -592,27 +703,78 @@ class SimulationProjectService: artifacts=artifacts, world_manifest=world_manifest, ) + except _SimulationProcessingCancelled: + pass except (GaussianPipelineGatewayError, SimulationProjectError, OSError) as exc: with suppress(SimulationProjectError): self.store.fail(project_id, str(exc)) finally: + with self._condition: + self._inflight_jobs.pop(project_id, None) if provider is not None: provider.close() def delete(self, project_id: str) -> None: project = self.store.get(project_id) - job_id = project["provider"].get("job_id") + with self._condition: + cancel = self._cancel_events.setdefault(project_id, threading.Event()) + cancel.set() + if project_id in self._queued_ids: + self._queue = deque(item for item in self._queue if item != project_id) + self._queued_ids.discard(project_id) + job_id = self._inflight_jobs.pop( + project_id, + project["provider"].get("job_id"), + ) + self._condition.notify_all() if isinstance(job_id, str): provider = self.provider_factory() if provider is None: + with suppress(SimulationProjectError): + self.store.fail( + project_id, + "Gaussian Pipeline недоступен для удаления серверных артефактов.", + ) raise SimulationProjectConflictError( "Gaussian Pipeline недоступен для удаления серверных артефактов." ) try: provider.delete_job(job_id) + except GaussianPipelineGatewayError: + with suppress(SimulationProjectError): + self.store.fail( + project_id, + "Не удалось удалить активную сборку на Worker 006.", + ) + raise finally: provider.close() - self.store.delete(project_id) + self.store.delete(project_id, allow_active=True) + with self._condition: + self._cancel_events.pop(project_id, None) + self._condition.notify_all() + + def _raise_if_cancelled(self, project_id: str) -> None: + with self._condition: + event = self._cancel_events.get(project_id) + if event is not None and event.is_set(): + raise _SimulationProcessingCancelled(project_id) + + def _register_inflight_job( + self, + project_id: str, + job_id: str, + provider: GaussianPipelineGateway, + ) -> None: + with self._condition: + event = self._cancel_events.get(project_id) + cancelled = event is not None and event.is_set() + if not cancelled: + self._inflight_jobs[project_id] = job_id + if cancelled: + with suppress(GaussianPipelineGatewayError): + provider.delete_job(job_id) + raise _SimulationProcessingCancelled(project_id) def _retry_provider_unavailable(operation: Callable[[], _T]) -> _T: @@ -650,13 +812,15 @@ def _artifact_descriptors(value: object) -> list[dict[str, Any]]: or not isinstance(role, str) ): raise SimulationProjectError("Gaussian artifact fields are invalid") - descriptors.append({ - "role": role, - "logical_path": logical_path, - "media_type": media_type, - "sha256": sha256, - "byte_length": byte_length, - }) + descriptors.append( + { + "role": role, + "logical_path": logical_path, + "media_type": media_type, + "sha256": sha256, + "byte_length": byte_length, + } + ) return descriptors @@ -666,16 +830,12 @@ def _world_manifest(project_id: str, artifacts: list[dict[str, Any]]) -> dict[st if descriptor is None: return None encoded = "/".join( - quote(part, safe="") - for part in str(descriptor["logical_path"]).split("/") + quote(part, safe="") for part in str(descriptor["logical_path"]).split("/") ) return f"/api/v1/simulation-worlds/projects/{project_id}/artifacts/{encoded}" - collision_mesh_url = ( - url_for("rover-collision-mesh") - or url_for("rover-terrain-mesh") - or url_for("collision-mesh") - ) + rover_collision_mesh_url = url_for("rover-collision-mesh") or url_for("rover-terrain-mesh") + collision_mesh_url = rover_collision_mesh_url or url_for("collision-mesh") return { "schema_version": WORLD_MANIFEST_SCHEMA, "project_id": project_id, @@ -689,39 +849,53 @@ def _world_manifest(project_id: str, artifacts: list[dict[str, Any]]) -> dict[st }, "transforms": { "world_from_visual": [1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1], - "world_from_collision": [-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1], + "world_from_collision": ( + [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] + if rover_collision_mesh_url is not None + else [-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1] + ), }, } -def _collision_profile(scene_type: str) -> dict[str, Any]: - """Build the portable walkable-volume contract around the scanner origin.""" - return { - "scene_type": scene_type, - "seed_position": [0, 1, 0], - "capsule_height": 1.6, - "capsule_radius": 0.2, - "voxel_size": 0.2 if scene_type == "outdoor" else 0.1, - "mesh_shape": "smooth", - } - - def _default_viewer_settings() -> dict[str, Any]: return { "schema_version": VIEWER_SETTINGS_SCHEMA, "quality": "high", - "visual": {"inverted": True, "axis": "x"}, - "collision": {"inverted": True, "axis": "y"}, + "visual": {"rotation_degrees": {"x": 180.0, "y": 0.0, "z": 0.0}}, + "collision": {"rotation_degrees": {"x": 0.0, "y": 0.0, "z": 0.0}}, "camera": { "invert_horizontal": True, "invert_vertical": False, }, + "ugv": _default_ugv_settings(), + } + + +def _default_ugv_settings() -> dict[str, Any]: + return { + "preset_name": "UGV 100 кг", + "mass_kg": 100.0, + "dimensions_m": { + "length": 1.0, + "width": 0.8, + "height": 0.4, + "ground_clearance": 0.15, + }, + "max_speed_mps": 1.2, + "max_turn_rate_degrees": 45.0, + "invert_steering": False, } def _viewer_settings(value: object) -> dict[str, Any]: if not isinstance(value, dict) or set(value) != { - "schema_version", "quality", "visual", "collision", "camera" + "schema_version", + "quality", + "visual", + "collision", + "camera", + "ugv", }: raise SimulationProjectError("simulation viewer settings contract is invalid") if value.get("schema_version") != VIEWER_SETTINGS_SCHEMA: @@ -732,23 +906,104 @@ def _viewer_settings(value: object) -> dict[str, Any]: def layer(name: str) -> dict[str, Any]: raw = value.get(name) - if not isinstance(raw, dict) or set(raw) != {"inverted", "axis"}: + if not isinstance(raw, dict) or set(raw) != {"rotation_degrees"}: raise SimulationProjectError(f"simulation {name} transform is invalid") - inverted = raw.get("inverted") - axis = raw.get("axis") - if not isinstance(inverted, bool) or axis not in {"x", "y", "z"}: + raw_rotation = raw.get("rotation_degrees") + if not isinstance(raw_rotation, dict) or set(raw_rotation) != {"x", "y", "z"}: raise SimulationProjectError(f"simulation {name} transform is invalid") - return {"inverted": inverted, "axis": axis} + rotation: dict[str, float] = {} + for axis in ("x", "y", "z"): + angle = raw_rotation.get(axis) + if ( + not isinstance(angle, (int, float)) + or isinstance(angle, bool) + or not math.isfinite(angle) + or angle < -360.0 + or angle > 360.0 + ): + raise SimulationProjectError(f"simulation {name} transform is invalid") + rotation[axis] = float(angle) + return {"rotation_degrees": rotation} camera = value.get("camera") - if not isinstance(camera, dict) or set(camera) != { - "invert_horizontal", "invert_vertical" - }: + if not isinstance(camera, dict) or set(camera) != {"invert_horizontal", "invert_vertical"}: raise SimulationProjectError("simulation camera settings are invalid") horizontal = camera.get("invert_horizontal") vertical = camera.get("invert_vertical") if not isinstance(horizontal, bool) or not isinstance(vertical, bool): raise SimulationProjectError("simulation camera settings are invalid") + + ugv = value.get("ugv") + if not isinstance(ugv, dict) or set(ugv) != { + "preset_name", + "mass_kg", + "dimensions_m", + "max_speed_mps", + "max_turn_rate_degrees", + "invert_steering", + }: + raise SimulationProjectError("simulation UGV settings are invalid") + preset_name = " ".join(str(ugv.get("preset_name", "")).split()) + if not 1 <= len(preset_name) <= 80: + raise SimulationProjectError("simulation UGV preset name is invalid") + dimensions = ugv.get("dimensions_m") + if not isinstance(dimensions, dict) or set(dimensions) != { + "length", + "width", + "height", + "ground_clearance", + }: + raise SimulationProjectError("simulation UGV dimensions are invalid") + + def bounded_number( + raw: object, + *, + minimum: float, + maximum: float, + label: str, + ) -> float: + if ( + not isinstance(raw, (int, float)) + or isinstance(raw, bool) + or not math.isfinite(raw) + or raw < minimum + or raw > maximum + ): + raise SimulationProjectError(f"simulation UGV {label} is invalid") + return float(raw) + + mass_kg = bounded_number( + ugv.get("mass_kg"), minimum=0.01, maximum=1_000_000_000, label="mass" + ) + length = bounded_number( + dimensions.get("length"), minimum=0.01, maximum=1_000_000_000, label="length" + ) + width = bounded_number( + dimensions.get("width"), minimum=0.01, maximum=1_000_000_000, label="width" + ) + height = bounded_number( + dimensions.get("height"), minimum=0.07, maximum=1_000_000_000, label="height" + ) + ground_clearance = bounded_number( + dimensions.get("ground_clearance"), + minimum=0.01, + maximum=1_000_000_000, + label="ground clearance", + ) + if ground_clearance >= height - 0.05: + raise SimulationProjectError("simulation UGV ground clearance exceeds its height") + max_speed_mps = bounded_number( + ugv.get("max_speed_mps"), minimum=0, maximum=1_000_000_000, label="maximum speed" + ) + max_turn_rate_degrees = bounded_number( + ugv.get("max_turn_rate_degrees"), + minimum=0, + maximum=1_000_000_000, + label="maximum turn rate", + ) + invert_steering = ugv.get("invert_steering") + if not isinstance(invert_steering, bool): + raise SimulationProjectError("simulation UGV steering inversion is invalid") return { "schema_version": VIEWER_SETTINGS_SCHEMA, "quality": quality, @@ -758,9 +1013,69 @@ def _viewer_settings(value: object) -> dict[str, Any]: "invert_horizontal": horizontal, "invert_vertical": vertical, }, + "ugv": { + "preset_name": preset_name, + "mass_kg": mass_kg, + "dimensions_m": { + "length": length, + "width": width, + "height": height, + "ground_clearance": ground_clearance, + }, + "max_speed_mps": max_speed_mps, + "max_turn_rate_degrees": max_turn_rate_degrees, + "invert_steering": invert_steering, + }, } +def _migrate_viewer_settings(value: object) -> dict[str, Any]: + if not isinstance(value, dict): + return _default_viewer_settings() + if value.get("schema_version") == VIEWER_SETTINGS_SCHEMA: + return _viewer_settings(value) + if value.get("schema_version") == "missioncore.simulation-viewer-settings/v2": + migrated_v2 = { + **value, + "schema_version": VIEWER_SETTINGS_SCHEMA, + "ugv": _default_ugv_settings(), + } + try: + return _viewer_settings(migrated_v2) + except SimulationProjectError: + return _default_viewer_settings() + if value.get("schema_version") != "missioncore.simulation-viewer-settings/v1": + return _default_viewer_settings() + + def legacy_layer(name: str) -> dict[str, Any]: + raw = value.get(name) + rotation = {"x": 0.0, "y": 0.0, "z": 0.0} + if isinstance(raw, dict) and raw.get("inverted") is True: + axis = raw.get("axis") + if axis in rotation: + rotation[axis] = 180.0 + return {"rotation_degrees": rotation} + + camera = value.get("camera") + if not isinstance(camera, dict): + camera = {} + migrated = { + "schema_version": VIEWER_SETTINGS_SCHEMA, + "quality": value.get("quality", "high"), + "visual": legacy_layer("visual"), + "collision": legacy_layer("collision"), + "camera": { + "invert_horizontal": camera.get("invert_horizontal", True), + "invert_vertical": camera.get("invert_vertical", False), + }, + "ugv": _default_ugv_settings(), + } + try: + return _viewer_settings(migrated) + except SimulationProjectError: + return _default_viewer_settings() + + def _project_name(value: str) -> str: normalized = " ".join(value.split()) if not 1 <= len(normalized) <= 120: @@ -768,6 +1083,18 @@ def _project_name(value: str) -> str: return normalized +def _provider_timestamp(value: str, label: str) -> str: + if PROVIDER_TIMESTAMP_PATTERN.fullmatch(value) is None: + raise SimulationProjectError(f"simulation provider {label} timestamp is invalid") + return value + + +def _optional_provider_timestamp(value: object) -> str | None: + if isinstance(value, str) and PROVIDER_TIMESTAMP_PATTERN.fullmatch(value) is not None: + return value + return None + + def _logical_path(value: object) -> str: if not isinstance(value, str) or not value or len(value) > 1024: raise SimulationProjectError("simulation logical path is invalid") diff --git a/src/k1link/web/simulation_projects_api.py b/src/k1link/web/simulation_projects_api.py index 97a8581..ecfe239 100644 --- a/src/k1link/web/simulation_projects_api.py +++ b/src/k1link/web/simulation_projects_api.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any, Literal -from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, Response +from fastapi import APIRouter, Header, HTTPException, Request, Response from fastapi.responses import FileResponse from pydantic import BaseModel, ConfigDict, Field @@ -46,11 +46,18 @@ class SimulationProjectUpdate(BaseModel): scene_type: Literal["interior", "outdoor", "object"] +class SimulationEulerRotation(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + x: float = Field(ge=-360, le=360) + y: float = Field(ge=-360, le=360) + z: float = Field(ge=-360, le=360) + + class SimulationLayerViewerSettings(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) - inverted: bool - axis: Literal["x", "y", "z"] + rotation_degrees: SimulationEulerRotation class SimulationCameraViewerSettings(BaseModel): @@ -60,14 +67,35 @@ class SimulationCameraViewerSettings(BaseModel): invert_vertical: bool +class SimulationUgvDimensions(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + length: float = Field(ge=0.01, le=1_000_000_000) + width: float = Field(ge=0.01, le=1_000_000_000) + height: float = Field(ge=0.07, le=1_000_000_000) + ground_clearance: float = Field(ge=0.01, le=1_000_000_000) + + +class SimulationUgvViewerSettings(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + preset_name: str = Field(min_length=1, max_length=80) + mass_kg: float = Field(ge=0.01, le=1_000_000_000) + dimensions_m: SimulationUgvDimensions + max_speed_mps: float = Field(ge=0, le=1_000_000_000) + max_turn_rate_degrees: float = Field(ge=0, le=1_000_000_000) + invert_steering: bool + + class SimulationViewerSettings(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) - schema_version: Literal["missioncore.simulation-viewer-settings/v1"] = VIEWER_SETTINGS_SCHEMA + schema_version: Literal["missioncore.simulation-viewer-settings/v3"] = VIEWER_SETTINGS_SCHEMA quality: Literal["low", "medium", "high", "ultra", "maximum"] visual: SimulationLayerViewerSettings collision: SimulationLayerViewerSettings camera: SimulationCameraViewerSettings + ugv: SimulationUgvViewerSettings class SimulationProjectDocument(BaseModel): @@ -162,11 +190,14 @@ def build_simulation_projects_router( def source_upload_state(project_id: str, file_id: str) -> Response: try: offset, byte_length = store.upload_state(project_id, file_id) - return Response(status_code=204, headers={ - "Upload-Offset": str(offset), - "Upload-Length": str(byte_length), - "Cache-Control": "no-store", - }) + return Response( + status_code=204, + headers={ + "Upload-Offset": str(offset), + "Upload-Length": str(byte_length), + "Cache-Control": "no-store", + }, + ) except SimulationProjectNotFoundError as exc: raise HTTPException(status_code=404, detail="Исходный файл не найден.") from exc @@ -199,11 +230,14 @@ def build_simulation_projects_router( source_file = next( item for item in project["source"]["files"] if item["file_id"] == file_id ) - return Response(status_code=204, headers={ - "Upload-Offset": str(source_file["uploaded_bytes"]), - "Upload-Length": str(source_file["byte_length"]), - "Cache-Control": "no-store", - }) + return Response( + status_code=204, + headers={ + "Upload-Offset": str(source_file["uploaded_bytes"]), + "Upload-Length": str(source_file["byte_length"]), + "Cache-Control": "no-store", + }, + ) except SimulationProjectNotFoundError as exc: raise HTTPException(status_code=404, detail="Исходный файл не найден.") from exc except SimulationProjectConflictError as exc: @@ -212,13 +246,10 @@ def build_simulation_projects_router( raise HTTPException(status_code=400, detail=str(exc)) from exc @router.post("/projects/{project_id}/build", response_model=SimulationProjectDocument) - def build_project( - project_id: str, - background_tasks: BackgroundTasks, - ) -> dict[str, Any]: + def build_project(project_id: str) -> dict[str, Any]: try: project = service.begin_build(project_id) - background_tasks.add_task(service.process, project_id) + service.enqueue(project_id) return project except SimulationProjectNotFoundError as exc: raise HTTPException(status_code=404, detail="Проект симуляции не найден.") from exc diff --git a/tests/test_simulation_projects.py b/tests/test_simulation_projects.py index 9c358a8..2069c21 100644 --- a/tests/test_simulation_projects.py +++ b/tests/test_simulation_projects.py @@ -1,6 +1,8 @@ from __future__ import annotations +import json from pathlib import Path +from threading import Event from typing import Any import pytest @@ -14,6 +16,7 @@ from k1link.simulation.gaussian_pipeline_gateway import ( ) from k1link.simulation.projects import ( SimulationProjectConflictError, + SimulationProjectNotFoundError, SimulationProjectService, SimulationProjectStore, _world_manifest, @@ -29,6 +32,22 @@ def _folder_files() -> list[dict[str, object]]: ] +def _default_ugv_settings() -> dict[str, object]: + return { + "preset_name": "UGV 100 кг", + "mass_kg": 100.0, + "dimensions_m": { + "length": 1.0, + "width": 0.8, + "height": 0.4, + "ground_clearance": 0.15, + }, + "max_speed_mps": 1.2, + "max_turn_rate_degrees": 45.0, + "invert_steering": False, + } + + def _upload_all(store: SimulationProjectStore, project: dict[str, Any]) -> None: payloads = { "export/scene.lcc": b'{"fileType":"Portable"}', @@ -44,6 +63,19 @@ def _upload_all(store: SimulationProjectStore, project: dict[str, Any]) -> None: ) +def test_store_accepts_four_gib_archive_without_allocating_payload(tmp_path: Path) -> None: + store = SimulationProjectStore(tmp_path) + project = store.create( + name="Four GiB archive", + scene_type="outdoor", + source_kind="archive", + files=[{"logical_path": "four-gib.7z", "byte_length": 4 * 1024**3}], + ) + + assert project["status"] == "uploading" + assert project["source"]["total_byte_length"] == 4 * 1024**3 + + def test_store_persists_resumable_folder_upload_and_state_transitions(tmp_path: Path) -> None: store = SimulationProjectStore(tmp_path) project = store.create( @@ -86,10 +118,43 @@ def test_store_persists_resumable_folder_upload_and_state_transitions(tmp_path: ) queued = store.begin_build(project["project_id"]) assert queued["status"] == "queued" + assert queued["provider"]["job_created_at_utc"] is None + assert queued["provider"]["state_started_at_utc"] is None with pytest.raises(SimulationProjectConflictError, match="active"): store.delete(project["project_id"]) +def test_store_preserves_provider_job_and_stage_timing(tmp_path: Path) -> None: + store = SimulationProjectStore(tmp_path) + project = store.create( + name="Timed scene", + scene_type="outdoor", + source_kind="folder", + files=_folder_files(), + ) + _upload_all(store, project) + store.begin_build(project["project_id"]) + + first = store.update_processing( + project["project_id"], + status="processing", + provider_job_id="gsp-20260826000000-deadbeef", + provider_job_created_at_utc="2026-08-26T09:45:14.000Z", + provider_state="building_streamed_sog", + provider_state_started_at_utc="2026-08-26T09:48:20.000Z", + ) + repeated = store.update_processing( + project["project_id"], + status="processing", + provider_state="building_streamed_sog", + provider_state_started_at_utc="2026-08-26T09:55:00.000Z", + ) + + assert first["provider"]["job_created_at_utc"] == "2026-08-26T09:45:14.000Z" + assert first["provider"]["state_started_at_utc"] == "2026-08-26T09:48:20.000Z" + assert repeated["provider"]["state_started_at_utc"] == "2026-08-26T09:48:20.000Z" + + class _ReadyProvider: def __init__(self) -> None: self.deleted: list[str] = [] @@ -128,6 +193,8 @@ class _ReadyProvider: "schema_version": "gaussian-pipeline.job/v1", "job_id": "gsp-20260826000000-deadbeef", "state": "queued", + "created_at_utc": "2026-08-26T09:45:14.000Z", + "updated_at_utc": "2026-08-26T09:45:14.000Z", } def get_job(self, _job_id: str) -> dict[str, object]: @@ -135,6 +202,8 @@ class _ReadyProvider: "schema_version": "gaussian-pipeline.job/v1", "job_id": "gsp-20260826000000-deadbeef", "state": "ready", + "created_at_utc": "2026-08-26T09:45:14.000Z", + "updated_at_utc": "2026-08-26T10:26:57.000Z", "progress": {"completed_steps": 4, "total_steps": 4}, } @@ -181,7 +250,7 @@ class _ReadyProvider: return None -def test_service_materializes_world_manifest_and_deletes_both_copies(tmp_path: Path) -> None: +def test_service_builds_visual_world_without_automatic_collision(tmp_path: Path) -> None: store = SimulationProjectStore(tmp_path) project = store.create( name="MOJJOKER", @@ -197,13 +266,44 @@ def test_service_materializes_world_manifest_and_deletes_both_copies(tmp_path: P ready = store.get(project["project_id"]) assert ready["status"] == "ready" + assert ready["provider"]["job_created_at_utc"] == "2026-08-26T09:45:14.000Z" assert ready["world_manifest"]["visual"]["preview_sog_url"].endswith("/preview.sog") assert ready["world_manifest"]["collision"]["available"] is False assert ready["world_manifest"]["transforms"]["world_from_visual"] == [ - 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, + 1, + 0, + 0, + 0, + 0, + -1, + 0, + 0, + 0, + 0, + -1, + 0, + 0, + 0, + 0, + 1, ] assert ready["world_manifest"]["transforms"]["world_from_collision"] == [ - -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, + -1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + -1, + 0, + 0, + 0, + 0, + 1, ] assert provider.upload_calls == 1 assert provider.submit_calls == 1 @@ -211,20 +311,70 @@ def test_service_materializes_world_manifest_and_deletes_both_copies(tmp_path: P assert provider.submitted_document["outputs"] == { "preview_sog": True, "streamed_sog": True, - "collision": True, - } - assert provider.submitted_document["collision_profile"] == { - "scene_type": "interior", - "seed_position": [0, 1, 0], - "capsule_height": 1.6, - "capsule_radius": 0.2, - "voxel_size": 0.1, - "mesh_shape": "smooth", + "collision": False, } + assert provider.submitted_document["collision_profile"] is None service.delete(project["project_id"]) assert provider.deleted == ["gsp-20260826000000-deadbeef"] +def test_service_queue_processes_projects_strictly_one_at_a_time(tmp_path: Path) -> None: + store = SimulationProjectStore(tmp_path) + projects: list[dict[str, Any]] = [] + for name in ("First scene", "Second scene"): + project = store.create( + name=name, + scene_type="outdoor", + source_kind="folder", + files=_folder_files(), + ) + _upload_all(store, project) + projects.append(store.begin_build(project["project_id"])) + + service = SimulationProjectService(store, provider_factory=lambda: None) + first_started = Event() + release_first = Event() + second_started = Event() + completed = Event() + order: list[str] = [] + + def process(project_id: str) -> None: + order.append(project_id) + if project_id == projects[0]["project_id"]: + first_started.set() + assert release_first.wait(1.0) + else: + second_started.set() + completed.set() + + service.process = process # type: ignore[method-assign] + service.enqueue(projects[0]["project_id"]) + assert first_started.wait(1.0) + service.enqueue(projects[1]["project_id"]) + assert not second_started.wait(0.05) + release_first.set() + assert completed.wait(1.0) + assert order == [projects[0]["project_id"], projects[1]["project_id"]] + + +def test_service_deletes_a_queued_project_before_worker_submission(tmp_path: Path) -> None: + store = SimulationProjectStore(tmp_path) + project = store.create( + name="Queued scene", + scene_type="outdoor", + source_kind="folder", + files=_folder_files(), + ) + _upload_all(store, project) + store.begin_build(project["project_id"]) + service = SimulationProjectService(store, provider_factory=lambda: None) + + service.delete(project["project_id"]) + + with pytest.raises(SimulationProjectNotFoundError, match="unavailable"): + store.get(project["project_id"]) + + def test_world_manifest_prefers_combined_rover_collision_world() -> None: manifest = _world_manifest( "sim-" + "a" * 32, @@ -245,9 +395,25 @@ def test_world_manifest_prefers_combined_rover_collision_world() -> None: ) assert manifest["collision"]["available"] is True - assert manifest["collision"]["mesh_url"].endswith( - "/artifacts/rover/scene.collision.glb" - ) + assert manifest["collision"]["mesh_url"].endswith("/artifacts/rover/scene.collision.glb") + assert manifest["transforms"]["world_from_collision"] == [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + ] def test_service_resumes_a_persisted_provider_job_without_reupload(tmp_path: Path) -> None: @@ -412,11 +578,12 @@ def test_ready_project_rebuilds_from_retained_source_and_preserves_viewer_settin ) # type: ignore[arg-type] service.process(project["project_id"]) settings = { - "schema_version": "missioncore.simulation-viewer-settings/v1", + "schema_version": "missioncore.simulation-viewer-settings/v3", "quality": "medium", - "visual": {"inverted": True, "axis": "z"}, - "collision": {"inverted": True, "axis": "y"}, + "visual": {"rotation_degrees": {"x": 0, "y": 0, "z": 90}}, + "collision": {"rotation_degrees": {"x": 0, "y": -90, "z": 0}}, "camera": {"invert_horizontal": False, "invert_vertical": True}, + "ugv": _default_ugv_settings(), } store.update_viewer_settings(project["project_id"], settings) @@ -434,21 +601,25 @@ def test_api_exposes_same_origin_resumable_upload_contract(tmp_path: Path) -> No app = FastAPI() app.include_router(build_simulation_projects_router(store=store, service=service)) client = TestClient(app) - created = client.post("/api/v1/simulation-worlds/projects", json={ - "schema_version": "missioncore.simulation-project-create/v1", - "name": "Archive scene", - "scene_type": "outdoor", - "source_kind": "archive", - "files": [{"logical_path": "scan.rar", "byte_length": 6}], - }) + created = client.post( + "/api/v1/simulation-worlds/projects", + json={ + "schema_version": "missioncore.simulation-project-create/v1", + "name": "Archive scene", + "scene_type": "outdoor", + "source_kind": "archive", + "files": [{"logical_path": "scan.rar", "byte_length": 6}], + }, + ) assert created.status_code == 201 project = created.json() assert project["viewer_settings"] == { - "schema_version": "missioncore.simulation-viewer-settings/v1", + "schema_version": "missioncore.simulation-viewer-settings/v3", "quality": "high", - "visual": {"inverted": True, "axis": "x"}, - "collision": {"inverted": True, "axis": "y"}, + "visual": {"rotation_degrees": {"x": 180.0, "y": 0.0, "z": 0.0}}, + "collision": {"rotation_degrees": {"x": 0.0, "y": 0.0, "z": 0.0}}, "camera": {"invert_horizontal": True, "invert_vertical": False}, + "ugv": _default_ugv_settings(), } source_file = project["source"]["files"][0] upload_url = ( @@ -467,11 +638,18 @@ def test_api_exposes_same_origin_resumable_upload_contract(tmp_path: Path) -> No assert catalog["projects"][0]["project_id"] == project["project_id"] settings = { - "schema_version": "missioncore.simulation-viewer-settings/v1", + "schema_version": "missioncore.simulation-viewer-settings/v3", "quality": "medium", - "visual": {"inverted": True, "axis": "z"}, - "collision": {"inverted": False, "axis": "x"}, + "visual": {"rotation_degrees": {"x": 0, "y": 0, "z": 90}}, + "collision": {"rotation_degrees": {"x": -90, "y": 0, "z": 0}}, "camera": {"invert_horizontal": False, "invert_vertical": True}, + "ugv": { + **_default_ugv_settings(), + "mass_kg": 1_000_000_000, + "max_speed_mps": 250.0, + "max_turn_rate_degrees": 720.0, + "invert_steering": True, + }, } saved = client.put( f"/api/v1/simulation-worlds/projects/{project['project_id']}/viewer-settings", @@ -479,7 +657,71 @@ def test_api_exposes_same_origin_resumable_upload_contract(tmp_path: Path) -> No ) assert saved.status_code == 200 assert saved.json()["viewer_settings"] == settings - persisted = client.get( - f"/api/v1/simulation-worlds/projects/{project['project_id']}" - ).json() + persisted = client.get(f"/api/v1/simulation-worlds/projects/{project['project_id']}").json() assert persisted["viewer_settings"] == settings + + +def test_catalog_migrates_legacy_layer_inversions_to_euler_rotations(tmp_path: Path) -> None: + store = SimulationProjectStore(tmp_path) + project = store.create( + name="Legacy scene", + scene_type="outdoor", + source_kind="folder", + files=_folder_files(), + ) + project_path = ( + tmp_path / "simulation-worlds" / "projects" / project["project_id"] / "project.json" + ) + document = json.loads(project_path.read_text(encoding="utf-8")) + document["viewer_settings"] = { + "schema_version": "missioncore.simulation-viewer-settings/v1", + "quality": "ultra", + "visual": {"inverted": True, "axis": "z"}, + "collision": {"inverted": False, "axis": "y"}, + "camera": {"invert_horizontal": False, "invert_vertical": True}, + } + project_path.write_text(json.dumps(document), encoding="utf-8") + + migrated = store.get(project["project_id"])["viewer_settings"] + + assert migrated == { + "schema_version": "missioncore.simulation-viewer-settings/v3", + "quality": "ultra", + "visual": {"rotation_degrees": {"x": 0.0, "y": 0.0, "z": 180.0}}, + "collision": {"rotation_degrees": {"x": 0.0, "y": 0.0, "z": 0.0}}, + "camera": {"invert_horizontal": False, "invert_vertical": True}, + "ugv": _default_ugv_settings(), + } + + +def test_catalog_adds_default_ugv_preset_to_v2_viewer_settings(tmp_path: Path) -> None: + store = SimulationProjectStore(tmp_path) + project = store.create( + name="Current scene", + scene_type="outdoor", + source_kind="folder", + files=_folder_files(), + ) + project_path = ( + tmp_path / "simulation-worlds" / "projects" / project["project_id"] / "project.json" + ) + document = json.loads(project_path.read_text(encoding="utf-8")) + document["viewer_settings"] = { + "schema_version": "missioncore.simulation-viewer-settings/v2", + "quality": "medium", + "visual": {"rotation_degrees": {"x": -180.0, "y": 0.0, "z": 0.0}}, + "collision": {"rotation_degrees": {"x": -90.0, "y": 0.0, "z": 0.0}}, + "camera": {"invert_horizontal": False, "invert_vertical": True}, + } + project_path.write_text(json.dumps(document), encoding="utf-8") + + migrated = store.get(project["project_id"])["viewer_settings"] + + assert migrated == { + "schema_version": "missioncore.simulation-viewer-settings/v3", + "quality": "medium", + "visual": {"rotation_degrees": {"x": -180.0, "y": 0.0, "z": 0.0}}, + "collision": {"rotation_degrees": {"x": -90.0, "y": 0.0, "z": 0.0}}, + "camera": {"invert_horizontal": False, "invert_vertical": True}, + "ugv": _default_ugv_settings(), + } From 2e44e27967df988e82a3f3b06108443a6738945b Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 28 Aug 2026 13:46:55 +0300 Subject: [PATCH 2/6] feat(simulation): auto-build source mesh collision --- docs/20_SIMULATION_WORLD_SURFACE_BRIEF.md | 39 +++--- .../simulation/gaussian_pipeline_gateway.py | 23 +++ src/k1link/simulation/projects.py | 21 ++- tests/test_gaussian_pipeline_gateway.py | 22 +++ tests/test_simulation_projects.py | 131 +++++++++++++++--- 5 files changed, 192 insertions(+), 44 deletions(-) diff --git a/docs/20_SIMULATION_WORLD_SURFACE_BRIEF.md b/docs/20_SIMULATION_WORLD_SURFACE_BRIEF.md index a7da688..51be5c8 100644 --- a/docs/20_SIMULATION_WORLD_SURFACE_BRIEF.md +++ b/docs/20_SIMULATION_WORLD_SURFACE_BRIEF.md @@ -58,12 +58,10 @@ Mission Core backend DC Gaussian Pipeline ├─ TUS bundle or archive admission ├─ secure ZIP/RAR/7z normalization - └─ native Vulkan SplatTransform visual build on Worker 006 - -Optional physical-mesh pipeline (separate job; experimental) - ├─ source-mesh discovery or explicit mesh generation - ├─ physics-oriented cleanup and geometry budget - └─ compressed, digest-bound publication + ├─ native Vulkan SplatTransform visual build on Worker 006 + └─ optional Mesh_Files/*.ply source-collision path + ├─ conservative small-hole repair and topology audit + └─ mandatory Draco, digest-bound publication ``` The browser never receives the Worker token. Mission Core does not embed archive-format behavior @@ -81,18 +79,18 @@ SplatTransform GPU command through a confined filesystem spool to the pinned nat on the RTX 4090. It does not install into or share the Python, CUDA, Triton or computer-vision environments on the host. -Project processing is split at a durable product boundary. The mandatory first job builds only the -preview and streamed Gaussian assets required for visual inspection. It never generates collision -geometry, so a location can reach `ready` without paying the time, GPU-memory and storage cost of a -physical mesh. +Project processing never generates collision geometry from Gaussian data by default. When the +normalized source contains exactly one PLY below `Mesh_Files`, the same queued build automatically +selects the provider's `source` collision profile. The original indexed mesh is retained; only +strictly admitted small internal boundary loops on approximately planar Z-up surfaces receive new +triangles. Vertices and existing faces are never moved, welded, smoothed, simplified or remeshed. +The derived GLB then passes the mandatory Draco publication gate and carries a separate repair +report. A source with no admitted PLY remains visual-only and still reaches `ready` normally. -Physical geometry is an optional second job started only after the visual world is ready. Its first -candidate source is a mesh already present in the uploaded export (for example a PLY in -`Mesh_Files`); generation from the Gaussian cloud is a fallback experiment, not the default path. -The second-stage contract, cleanup method and acceptance gates are intentionally separate from the -visual build. When that stage publishes a GLB, simplification still controls decoded physics cost -and Draco controls transfer/storage bytes; compression is not treated as a replacement for a -physics mesh budget. +The conservative repair rejects outer borders, branched boundaries, large or non-planar loops, +vertical openings, mixed orientation and failed/self-intersecting triangulations. Ambiguous holes +remain open and are counted in the report instead of being silently capped. Explicit mesh +generation from Gaussian data remains a later experiment, not a fallback in this ingestion path. Visual and collision layers retain independent X/Y/Z correction settings, while PlayCanvas world, camera, navigation and future physics stay in the canonical Y-up coordinate system. Quality, both @@ -115,9 +113,10 @@ Primary implementation references: - encrypted, linked, traversing, duplicate and over-limit archive entries fail closed; - project status is durable and reflects provider state without fabricated percentages; - ready artifacts are imported digest-bound and served from Mission Core same-origin URLs; -- the mandatory build requests preview and streamed Gaussian outputs with collision disabled; -- a visual project reaches ready state without a collision artifact; -- physical mesh preparation is a separate explicit job and never blocks visual inspection; +- the build always requests preview and streamed Gaussian outputs; +- exactly one `Mesh_Files/*.ply` automatically selects repaired source-mesh collision plus Draco; +- archives without that mesh remain visual-only and reach ready without a collision artifact; +- generated Gaussian/voxel collision is never used as an implicit fallback; - edit changes project metadata; delete removes both the Mission Core project and terminal provider job; - a ready project mounts direct PlayCanvas Engine and loads Streamed SOG with preview fallback; diff --git a/src/k1link/simulation/gaussian_pipeline_gateway.py b/src/k1link/simulation/gaussian_pipeline_gateway.py index 848e9b9..fe2b78c 100644 --- a/src/k1link/simulation/gaussian_pipeline_gateway.py +++ b/src/k1link/simulation/gaussian_pipeline_gateway.py @@ -767,6 +767,24 @@ def _discover_bundle_members(root: Path, entrypoint: str, source_format: str) -> return _logical_path(logical, "descriptor member") members = {entrypoint} + source_meshes: list[str] = [] + for candidate in root.rglob("*"): + if candidate.is_symlink(): + raise GaussianPipelineIntegrityError( + "Gaussian source bundle contains a symlink" + ) + if not candidate.is_file(): + continue + logical_path = candidate.relative_to(root).as_posix() + if is_xgrids_source_mesh_path(logical_path): + source_meshes.append(logical_path) + source_meshes.sort(key=lambda value: value.encode("utf-8")) + if len(source_meshes) > 1: + raise GaussianPipelineIntegrityError( + "Gaussian source bundle must contain at most one PLY mesh inside Mesh_Files" + ) + if source_meshes: + members.add(source_meshes[0]) if source_format == "lcc": members.update({related("index.bin"), related("data.bin")}) file_type = document.get("fileType") @@ -810,6 +828,11 @@ def _discover_bundle_members(root: Path, entrypoint: str, source_format: str) -> return members +def is_xgrids_source_mesh_path(logical_path: str) -> bool: + parts = logical_path.lower().replace("\\", "/").split("/") + return "mesh_files" in parts and parts[-1].endswith(".ply") + + def _logical_path(value: str, label: str) -> str: if ( not value diff --git a/src/k1link/simulation/projects.py b/src/k1link/simulation/projects.py index 41a4ca3..4db6700 100644 --- a/src/k1link/simulation/projects.py +++ b/src/k1link/simulation/projects.py @@ -26,6 +26,7 @@ from k1link.simulation.gaussian_pipeline_gateway import ( GaussianPipelineUnavailableError, configured_gaussian_pipeline_gateway, discover_gaussian_source_bundle, + is_xgrids_source_mesh_path, ) PROJECT_SCHEMA: Final = "missioncore.simulation-project/v1" @@ -610,6 +611,22 @@ class SimulationProjectService: entrypoint=entrypoint, source_format=source_format, ) + source_mesh_available = any( + is_xgrids_source_mesh_path(member.logical_path) + for member in source.members + ) + collision_profile = ( + { + "scene_type": project["scene_type"], + "seed_position": [0.0, 0.0, 0.0], + "capsule_height": 0.4, + "capsule_radius": 0.4, + "voxel_size": 0.05, + "mesh_shape": "source", + } + if source_mesh_available + else None + ) request = { "schema_version": BUILD_REQUEST_SCHEMA, "idempotency_key": f"missioncore-{project_id}", @@ -617,10 +634,10 @@ class SimulationProjectService: "outputs": { "preview_sog": True, "streamed_sog": True, - "collision": False, + "collision": source_mesh_available, }, "preview_lod": "coarsest", - "collision_profile": None, + "collision_profile": collision_profile, } submitted = provider.submit_build(request) job_id = submitted.get("job_id") diff --git a/tests/test_gaussian_pipeline_gateway.py b/tests/test_gaussian_pipeline_gateway.py index 272bbcb..1537e95 100644 --- a/tests/test_gaussian_pipeline_gateway.py +++ b/tests/test_gaussian_pipeline_gateway.py @@ -117,6 +117,28 @@ def test_gateway_uploads_lcc_bundle_with_tus_and_reads_provider_contract(tmp_pat assert descriptor.total_byte_length == sum(member.byte_length for member in descriptor.members) +def test_folder_discovery_retains_one_nested_xgrids_source_mesh(tmp_path: Path) -> None: + root = tmp_path / "export" + scene = root / "LCC_Results" + mesh = root / "Mesh_Files" + scene.mkdir(parents=True) + mesh.mkdir() + (scene / "scan.lcc").write_text( + json.dumps({"fileType": "Portable"}), + encoding="utf-8", + ) + (scene / "index.bin").write_bytes(b"index") + (scene / "data.bin").write_bytes(b"data") + (mesh / "scan.ply").write_bytes(b"ply") + + members = _discover_bundle_members(root, "LCC_Results/scan.lcc", "lcc") + + assert "Mesh_Files/scan.ply" in members + (mesh / "duplicate.ply").write_bytes(b"ply") + with pytest.raises(GaussianPipelineIntegrityError, match="at most one"): + _discover_bundle_members(root, "LCC_Results/scan.lcc", "lcc") + + def test_gateway_uploads_and_normalizes_archive_with_tus(tmp_path: Path) -> None: archive_bytes = b"portable-archive" archive_sha = hashlib.sha256(archive_bytes).hexdigest() diff --git a/tests/test_simulation_projects.py b/tests/test_simulation_projects.py index 2069c21..cb33426 100644 --- a/tests/test_simulation_projects.py +++ b/tests/test_simulation_projects.py @@ -156,11 +156,12 @@ def test_store_preserves_provider_job_and_stage_timing(tmp_path: Path) -> None: class _ReadyProvider: - def __init__(self) -> None: + def __init__(self, *, source_mesh: bool = False) -> None: self.deleted: list[str] = [] self.upload_calls = 0 self.submit_calls = 0 self.submitted_document: dict[str, object] | None = None + self.source_mesh = source_mesh def capabilities(self) -> dict[str, object]: return {"outputs": ["preview.sog", "streamed-sog"]} @@ -173,17 +174,26 @@ class _ReadyProvider: source_format: str, ) -> GaussianSourceBundleUpload: self.upload_calls += 1 - members = ( + members = [ GaussianSourceMemberUpload("upload-1", entrypoint, "a" * 64, 23), GaussianSourceMemberUpload("upload-2", "export/data.bin", "b" * 64, 4), GaussianSourceMemberUpload("upload-3", "export/index.bin", "c" * 64, 5), - ) + ] + if self.source_mesh: + members.append( + GaussianSourceMemberUpload( + "upload-4", + "export/Mesh_Files/scene.ply", + "e" * 64, + 3, + ) + ) return GaussianSourceBundleUpload( format=source_format, entrypoint=entrypoint, bundle_sha256="d" * 64, - total_byte_length=32, - members=members, + total_byte_length=sum(member.byte_length for member in members), + members=tuple(members), ) def submit_build(self, document: dict[str, object]) -> dict[str, object]: @@ -208,6 +218,39 @@ class _ReadyProvider: } def get_result(self, _job_id: str) -> dict[str, object]: + artifacts = [ + { + "role": "preview", + "logical_path": "preview.sog", + "media_type": "application/octet-stream", + "sha256": "1" * 64, + "byte_length": 7, + }, + { + "role": "stream-manifest", + "logical_path": "streamed/lod-meta.json", + "media_type": "application/json", + "sha256": "2" * 64, + "byte_length": 2, + }, + ] + if self.source_mesh: + artifacts.extend([ + { + "role": "collision-mesh", + "logical_path": "collision/scene.collision.glb", + "media_type": "model/gltf-binary", + "sha256": "3" * 64, + "byte_length": 3, + }, + { + "role": "collision-repair-report", + "logical_path": "collision/scene.repair.json", + "media_type": "application/json", + "sha256": "4" * 64, + "byte_length": 2, + }, + ]) return { "schema_version": "gaussian-pipeline.build-result/v1", "job_id": "gsp-20260826000000-deadbeef", @@ -215,22 +258,7 @@ class _ReadyProvider: "source_revision": "e" * 40, "image_digest": f"sha256:{'f' * 64}", }, - "artifacts": [ - { - "role": "preview", - "logical_path": "preview.sog", - "media_type": "application/octet-stream", - "sha256": "1" * 64, - "byte_length": 7, - }, - { - "role": "stream-manifest", - "logical_path": "streamed/lod-meta.json", - "media_type": "application/json", - "sha256": "2" * 64, - "byte_length": 2, - }, - ], + "artifacts": artifacts, } def download_artifact( @@ -240,7 +268,13 @@ class _ReadyProvider: destination: Path, ) -> Path: destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_bytes(b"preview" if descriptor["role"] == "preview" else b"{}") + payload = { + "preview": b"preview", + "stream-manifest": b"{}", + "collision-mesh": b"glb", + "collision-repair-report": b"{}", + }[str(descriptor["role"])] + destination.write_bytes(payload) return destination def delete_job(self, job_id: str) -> None: @@ -318,6 +352,59 @@ def test_service_builds_visual_world_without_automatic_collision(tmp_path: Path) assert provider.deleted == ["gsp-20260826000000-deadbeef"] +def test_service_automatically_builds_repaired_source_mesh_collision(tmp_path: Path) -> None: + store = SimulationProjectStore(tmp_path) + files = [ + *_folder_files(), + {"logical_path": "export/Mesh_Files/scene.ply", "byte_length": 3}, + ] + project = store.create( + name="Source mesh scene", + scene_type="outdoor", + source_kind="folder", + files=files, + ) + payloads = { + "export/scene.lcc": b'{"fileType":"Portable"}', + "export/index.bin": b"index", + "export/data.bin": b"data", + "export/Mesh_Files/scene.ply": b"ply", + } + for source_file in project["source"]["files"]: + store.append_upload( + project["project_id"], + source_file["file_id"], + offset=0, + payload=payloads[source_file["logical_path"]], + ) + store.begin_build(project["project_id"]) + provider = _ReadyProvider(source_mesh=True) + service = SimulationProjectService(store, provider_factory=lambda: provider) # type: ignore[arg-type] + + service.process(project["project_id"]) + + ready = store.get(project["project_id"]) + assert ready["status"] == "ready" + assert ready["world_manifest"]["collision"]["available"] is True + assert ready["world_manifest"]["collision"]["mesh_url"].endswith( + "/collision/scene.collision.glb" + ) + assert provider.submitted_document is not None + assert provider.submitted_document["outputs"] == { + "preview_sog": True, + "streamed_sog": True, + "collision": True, + } + assert provider.submitted_document["collision_profile"] == { + "scene_type": "outdoor", + "seed_position": [0.0, 0.0, 0.0], + "capsule_height": 0.4, + "capsule_radius": 0.4, + "voxel_size": 0.05, + "mesh_shape": "source", + } + + def test_service_queue_processes_projects_strictly_one_at_a_time(tmp_path: Path) -> None: store = SimulationProjectStore(tmp_path) projects: list[dict[str, Any]] = [] From 45019f895290396b66f2aa3258c4735bc713b344 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 29 Aug 2026 13:36:37 +0300 Subject: [PATCH 3/6] feat(lab): seal full RAVNOVES004TREE semantic review --- .../src/core/laboratory/vegetationShadow.ts | 295 ++++++++++- .../laboratory/VegetationShadowResult.tsx | 234 ++++++++- .../test/vegetationShadow.test.mjs | 91 +++- ...-ravnoves004tree-full-video-source-v1.json | 19 + .../Invoke-E4FullSessionSegmentation.ps1 | 74 ++- .../Invoke-LabV1VegetationGooseBenchmark.ps1 | 136 ++++- scripts/seal_mixed_route_full_video_review.py | 38 ++ .../mixed_route_vegetation_review.py | 467 +++++++++++++++++- src/k1link/web/vegetation_shadow_lab_api.py | 68 +++ tests/test_e4_full_session_segmentation.py | 17 + .../test_lab_v1_goose_vegetation_benchmark.py | 24 + tests/test_vegetation_shadow_lab.py | 61 +++ 12 files changed, 1494 insertions(+), 30 deletions(-) create mode 100644 config/perception/lab-v1-ravnoves004tree-full-video-source-v1.json create mode 100644 scripts/seal_mixed_route_full_video_review.py diff --git a/apps/control-station/src/core/laboratory/vegetationShadow.ts b/apps/control-station/src/core/laboratory/vegetationShadow.ts index 08d8669..0b7cf5d 100644 --- a/apps/control-station/src/core/laboratory/vegetationShadow.ts +++ b/apps/control-station/src/core/laboratory/vegetationShadow.ts @@ -106,6 +106,39 @@ export interface VegetationMixedRouteReview { cases: readonly VegetationMixedRouteCase[]; } +export interface VegetationFullRouteLayer { + name: string; + resultId: string; + frameCount: 6830; + taxonomy: readonly VegetationVideoSemanticClass[]; + inferenceFps: number; + latencyP95Ms: number; + peakReservedVramBytes: number; +} + +export interface VegetationFullRouteReview { + sourceId: "RAVNOVES004TREE"; + sessionId: "20260828T130511Z_viewer_live"; + sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0"; + sourceJobInputSha256: string; + sourceStreamSha256: string; + recordedMediaSourceId: "recorded.camera.6a3945242828a038"; + recordedMediaGenerationSha256: string; + frameCount: 6830; + width: 800; + height: 600; + timelineStartSeconds: number; + timelineEndSeconds: number; + frameSourceTimesNs: readonly number[]; + decodeRepair: { + repairedFrameCount: 1; + sequence: 6092; + method: "duplicate-previous-decoded-frame"; + }; + city: VegetationFullRouteLayer; + vegetation: VegetationFullRouteLayer; +} + export interface VegetationShadowResult { resultId: string; createdAtUtc: string; @@ -116,6 +149,7 @@ export interface VegetationShadowResult { validationCases: readonly VegetationVisualCase[]; routeVideo: VegetationRouteVideo | null; routeReview: VegetationMixedRouteReview | null; + routeFullReview: VegetationFullRouteReview | null; limitations: readonly string[]; visualShadowReady: true; missionPolicyReadyForConfiguration: true; @@ -545,6 +579,246 @@ function mixedRouteReviewValue( }; } +function fullRouteTaxonomyValue( + value: unknown, + label: string, + schema: string, + classCount: number, +): readonly VegetationVideoSemanticClass[] { + const taxonomy = objectValue(value, `${label}.taxonomy`); + exact(taxonomy.schema_version, schema, `${label}.taxonomy.schema`); + const classes = arrayValue(taxonomy.classes, `${label}.taxonomy.classes`).map( + (raw, expectedId): VegetationVideoSemanticClass => { + const item = objectValue(raw, `${label}.taxonomy[${expectedId}]`); + const classId = integerValue(item.class_id, `${label}.class_id[${expectedId}]`); + if (classId !== expectedId) { + throw new VegetationShadowContractError(`${label}: taxonomy order changed.`); + } + const color = arrayValue(item.color_rgb, `${label}.color[${expectedId}]`) + .map((channel, index) => integerValue(channel, `${label}.color[${expectedId}][${index}]`)); + if (color.length !== 3 || color.some((channel) => channel > 255)) { + throw new VegetationShadowContractError(`${label}: taxonomy color invalid.`); + } + const disposition = item.disposition; + if ( + disposition !== "labeled" + && disposition !== "ambiguous" + && disposition !== "prediction" + && disposition !== "undefined" + ) { + throw new VegetationShadowContractError(`${label}: taxonomy disposition changed.`); + } + return { + classId, + label: textValue(item.label, `${label}.label[${expectedId}]`), + colorRgb: color as unknown as readonly [number, number, number], + disposition, + materialClass: item.material_class === null || item.material_class === undefined + ? null + : textValue(item.material_class, `${label}.material[${expectedId}]`), + evidenceState: item.evidence_state === null || item.evidence_state === undefined + ? null + : textValue(item.evidence_state, `${label}.evidence[${expectedId}]`), + }; + }, + ); + if (classes.length !== classCount) { + throw new VegetationShadowContractError(`${label}: taxonomy size changed.`); + } + return classes; +} + +function fullRouteLayerValue( + value: unknown, + layer: "city" | "vegetation", +): VegetationFullRouteLayer { + const label = `vegetation.route_full_review.layers.${layer}`; + const row = objectValue(value, label); + const resultId = textValue(row.result_id, `${label}.result_id`); + const identity = layer === "city" + ? /^result-[a-f0-9]{64}$/ + : /^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$/; + if (!identity.test(resultId)) { + throw new VegetationShadowContractError(`${label}: identity invalid.`); + } + exact(row.frame_count, 6830, `${label}.frame_count`); + const archive = objectValue(row.mask_archive, `${label}.mask_archive`); + exact( + archive.path, + layer === "city" ? "video/eomt-semantic-masks.zip" : "video/ddrnet-semantic-masks.zip", + `${label}.mask_archive.path`, + ); + const digest = textValue(archive.sha256, `${label}.mask_archive.sha256`); + if (!SHA256.test(digest)) { + throw new VegetationShadowContractError(`${label}: archive digest invalid.`); + } + integerValue(archive.byte_length, `${label}.mask_archive.byte_length`); + return { + name: textValue(row.name, `${label}.name`), + resultId, + frameCount: 6830, + taxonomy: fullRouteTaxonomyValue( + row.taxonomy, + label, + layer === "city" + ? "missioncore.recorded-eomt-taxonomy/v1" + : "missioncore.lab-v1-vegetation-taxonomy/v1", + layer === "city" ? 16 : 64, + ), + inferenceFps: numberValue(row.inference_fps, `${label}.inference_fps`), + latencyP95Ms: numberValue(row.latency_p95_ms, `${label}.latency_p95_ms`), + peakReservedVramBytes: integerValue( + row.peak_reserved_vram_bytes, + `${label}.peak_reserved_vram_bytes`, + ), + }; +} + +function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null { + if (value === null || value === undefined) return null; + const row = objectValue(value, "vegetation.route_full_review"); + exact(row.source_id, "RAVNOVES004TREE", "vegetation.route_full_review.source_id"); + exact( + row.session_id, + "20260828T130511Z_viewer_live", + "vegetation.route_full_review.session_id", + ); + exact( + row.source_job_id, + "recorded-camera-eb2783c5480d56bda07c8af0", + "vegetation.route_full_review.source_job_id", + ); + exact(row.frame_count, 6830, "vegetation.route_full_review.frame_count"); + exact(row.width, 800, "vegetation.route_full_review.width"); + exact(row.height, 600, "vegetation.route_full_review.height"); + exact(row.ground_truth, false, "vegetation.route_full_review.ground_truth"); + const sourceJobInputSha256 = textValue( + row.source_job_input_sha256, + "vegetation.route_full_review.source_job_input_sha256", + ); + const sourceStreamSha256 = textValue( + row.source_stream_sha256, + "vegetation.route_full_review.source_stream_sha256", + ); + exact( + sourceJobInputSha256, + "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715", + "vegetation.route_full_review.source_job_input_sha256", + ); + exact( + sourceStreamSha256, + "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac", + "vegetation.route_full_review.source_stream_sha256", + ); + exact( + row.recorded_media_source_id, + "recorded.camera.6a3945242828a038", + "vegetation.route_full_review.recorded_media_source_id", + ); + const recordedMediaGenerationSha256 = textValue( + row.recorded_media_generation_sha256, + "vegetation.route_full_review.recorded_media_generation_sha256", + ); + exact( + recordedMediaGenerationSha256, + "b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded", + "vegetation.route_full_review.recorded_media_generation_sha256", + ); + if ( + !SHA256.test(sourceJobInputSha256) + || !SHA256.test(sourceStreamSha256) + || !SHA256.test(recordedMediaGenerationSha256) + ) { + throw new VegetationShadowContractError("vegetation.route_full_review: source digest invalid."); + } + const timelineStartSeconds = numberValue( + row.timeline_start_seconds, + "vegetation.route_full_review.timeline_start_seconds", + ); + const timelineEndSeconds = numberValue( + row.timeline_end_seconds, + "vegetation.route_full_review.timeline_end_seconds", + ); + if (timelineEndSeconds <= timelineStartSeconds) { + throw new VegetationShadowContractError("vegetation.route_full_review: timeline invalid."); + } + const frameSourceTimesNs = arrayValue( + row.frame_source_times_ns, + "vegetation.route_full_review.frame_source_times_ns", + ).map((raw, index) => { + const time = integerValue(raw, `vegetation.route_full_review.frame_source_times_ns[${index}]`); + if (!Number.isSafeInteger(time)) { + throw new VegetationShadowContractError("vegetation.route_full_review: unsafe frame time."); + } + return time; + }); + if ( + frameSourceTimesNs.length !== 6830 + || frameSourceTimesNs.some((time, index) => index > 0 && time <= frameSourceTimesNs[index - 1]!) + ) { + throw new VegetationShadowContractError("vegetation.route_full_review: frame timeline changed."); + } + const decodeRepair = objectValue( + row.decode_repair, + "vegetation.route_full_review.decode_repair", + ); + exact(decodeRepair.repaired_frame_count, 1, "vegetation.route_full_review.decode_repair.count"); + exact(decodeRepair.sequence, 6092, "vegetation.route_full_review.decode_repair.sequence"); + exact( + decodeRepair.method, + "duplicate-previous-decoded-frame", + "vegetation.route_full_review.decode_repair.method", + ); + const repairProofs = objectValue( + decodeRepair.proofs, + "vegetation.route_full_review.decode_repair.proofs", + ); + for (const [key, expectedPath] of Object.entries({ + eomt: "proofs/decode_repair.json", + ddrnet: "proofs/ddrnet_decode_repair.json", + })) { + const proof = objectValue( + repairProofs[key], + `vegetation.route_full_review.decode_repair.proofs.${key}`, + ); + exact( + proof.path, + expectedPath, + `vegetation.route_full_review.decode_repair.proofs.${key}.path`, + ); + const digest = textValue( + proof.sha256, + `vegetation.route_full_review.decode_repair.proofs.${key}.sha256`, + ); + if (!SHA256.test(digest)) { + throw new VegetationShadowContractError("vegetation.route_full_review: repair proof invalid."); + } + } + const layers = objectValue(row.layers, "vegetation.route_full_review.layers"); + return { + sourceId: "RAVNOVES004TREE", + sessionId: "20260828T130511Z_viewer_live", + sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0", + sourceJobInputSha256, + sourceStreamSha256, + recordedMediaSourceId: "recorded.camera.6a3945242828a038", + recordedMediaGenerationSha256, + frameCount: 6830, + width: 800, + height: 600, + timelineStartSeconds, + timelineEndSeconds, + frameSourceTimesNs, + decodeRepair: { + repairedFrameCount: 1, + sequence: 6092, + method: "duplicate-previous-decoded-frame", + }, + city: fullRouteLayerValue(layers.city, "city"), + vegetation: fullRouteLayerValue(layers.vegetation, "vegetation"), + }; +} + function parseResult( value: unknown, resultId: string, @@ -589,9 +863,10 @@ function parseResult( const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose") .map((item) => visualCaseValue(item, resultId, "goose", endpointRoot)); const routeReview = mixedRouteReviewValue(payload.route_review, resultId, endpointRoot); + const routeFullReview = fullRouteReviewValue(payload.route_full_review); if ( routeCases.length !== 0 - || (routeReview ? validationCases.length !== 0 : validationCases.length !== 12) + || (routeReview || routeFullReview ? validationCases.length !== 0 : validationCases.length !== 12) ) { throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer."); } @@ -605,6 +880,7 @@ function parseResult( validationCases, routeVideo: routeVideoValue(payload.route_video), routeReview, + routeFullReview, limitations: arrayValue(payload.limitations, "vegetation.limitations") .map((item, index) => textValue(item, `vegetation.limitations[${index}]`)), visualShadowReady: true, @@ -625,6 +901,23 @@ export function vegetationVideoMaskUrl(resultId: string, sequence: number): stri return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/masks/${sequence}`; } +export function vegetationFullRouteMaskUrl( + resultId: string, + layer: "city" | "vegetation", + sequence: number, +): string { + if ( + !RESULT_ID.test(resultId) + || (layer !== "city" && layer !== "vegetation") + || !Number.isInteger(sequence) + || sequence < 0 + || sequence >= 6830 + ) { + throw new VegetationShadowContractError("Vegetation full-route mask identity недопустима."); + } + return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-masks/${layer}/${sequence}`; +} + export async function fetchVegetationShadowResult( resultId: string, { diff --git a/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx b/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx index 3243ccb..642e3e2 100644 --- a/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx +++ b/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx @@ -1,7 +1,8 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react"; import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer"; +import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer"; import { LaboratoryEvidence, LaboratoryResultSummary, @@ -9,10 +10,21 @@ import { LaboratoryWorkTemplate, } from "../../components/laboratory/LaboratoryPresentation"; import { + RecordedEvidenceSemanticMaskOverlay, + type RecordedEvidenceSemanticClass, + type RecordedEvidenceSemanticPaletteEntry, +} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay"; +import { + vegetationFullRouteMaskUrl, vegetationVideoMaskUrl, + type VegetationFullRouteLayer, + type VegetationFullRouteReview, type VegetationMixedRouteReview, type VegetationShadowResult, } from "../../core/laboratory/vegetationShadow"; +import { recordedObservationSources } from "../../core/observation/recordedObservationSources"; +import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions"; +import type { ObservationSourceDescriptor } from "../../core/runtime/contracts"; import { fetchM49TgsFullShadowResult, type M49TgsFullShadowResult, @@ -31,6 +43,217 @@ const MIXED_ROUTE_MODES = [ { value: "tgs", label: "TGS" }, ] as const; +const FULL_ROUTE_MODES = [ + { value: "source", label: "SOURCE" }, + { value: "city", label: "ГОРОД · EoMT" }, + { value: "vegetation", label: "ПРИРОДА · DDRNet" }, +] as const; + +function semanticPresentation(layer: VegetationFullRouteLayer): { + classes: readonly RecordedEvidenceSemanticClass[]; + palette: readonly RecordedEvidenceSemanticPaletteEntry[]; +} { + return { + classes: layer.taxonomy.map((item) => ({ id: item.classId, label: item.label })), + palette: layer.taxonomy.map((item) => ({ + classId: item.classId, + color: item.classId === 0 + ? { kind: "transparent" as const } + : { kind: "diagnostic" as const, rgb: item.colorRgb }, + })), + }; +} + +function FullRouteReviewEvidence({ + resultId, + review, +}: { + resultId: string; + review: VegetationFullRouteReview; +}) { + const [sequence, setSequence] = useState(1); + const [playing, setPlaying] = useState(false); + const [playbackRate, setPlaybackRate] = useState(1); + const [mode, setMode] = useState("vegetation"); + const [expanded, setExpanded] = useState(false); + const [videoSource, setVideoSource] = useState(null); + const [videoError, setVideoError] = useState(null); + const frames = useMemo( + () => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({ + sequence: index + 1, + sourceTimeNs, + })), + [review.frameSourceTimesNs], + ); + const layer = mode === "source" ? null : review[mode]; + const semantic = useMemo(() => layer ? semanticPresentation(layer) : null, [layer]); + const maskSequence = sequence - 1; + const prefetchSrcs = useMemo(() => layer + ? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1) + .filter((candidate) => candidate < review.frameCount) + .map((candidate) => vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", candidate)) + : [], [layer, maskSequence, mode, resultId, review.frameCount]); + + useEffect(() => { + const controller = new AbortController(); + setVideoSource(null); + setVideoError(null); + void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal }) + .then((launch) => { + const source = recordedObservationSources(launch).find((candidate) => ( + candidate.id === review.recordedMediaSourceId + && candidate.modality === "video" + && candidate.semanticChannelId === "camera.video.recorded" + && candidate.delivery?.kind === "recorded-fmp4-manifest" + && candidate.delivery.manifestGenerationSha256 === review.recordedMediaGenerationSha256 + && candidate.delivery.timelineStartSeconds === review.timelineStartSeconds + && candidate.delivery.timelineEndSeconds >= review.timelineEndSeconds + )); + if (!source) { + throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline."); + } + if (!controller.signal.aborted) setVideoSource(source); + }) + .catch((caught: unknown) => { + if (!controller.signal.aborted) { + setVideoError(caught instanceof Error ? caught.message : "Записанное видео недоступно."); + } + }); + return () => controller.abort(); + }, [ + review.recordedMediaGenerationSha256, + review.recordedMediaSourceId, + review.sessionId, + review.timelineEndSeconds, + review.timelineStartSeconds, + ]); + + return ( + + {videoSource ? ( + +
+ {mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`} +
+ {layer && semantic ? ( + + ) : null} + + )} + /> + ) : ( +
+ {videoError ?? "Открываем автономный recorded source…"} +
+ )} +
+ ); +} + +function FullRouteReviewResult({ + rigLabel, + resultId, + review, +}: { + rigLabel: string; + resultId: string; + review: VegetationFullRouteReview; +}) { + return ( + + )} + evidence={( + + + + )} + result={( + + )} + /> + ); +} + function MixedRouteReviewEvidence({ review }: { review: VegetationMixedRouteReview }) { const [index, setIndex] = useState(0); const [mode, setMode] = useState("vegetation"); @@ -224,6 +447,15 @@ export function VegetationShadowResultView({ rigLabel: string; result: VegetationShadowResult; }) { + if (result.routeFullReview) { + return ( + + ); + } if (result.routeReview) { return ; } diff --git a/apps/control-station/test/vegetationShadow.test.mjs b/apps/control-station/test/vegetationShadow.test.mjs index cb12e72..5c61cb7 100644 --- a/apps/control-station/test/vegetationShadow.test.mjs +++ b/apps/control-station/test/vegetationShadow.test.mjs @@ -7,6 +7,7 @@ import { createServer } from "vite"; let server; let fetchVegetationBenchmarkResult; let fetchVegetationShadowResult; +let vegetationFullRouteMaskUrl; before(async () => { server = await createServer({ @@ -14,7 +15,11 @@ before(async () => { logLevel: "silent", server: { middlewareMode: true }, }); - ({ fetchVegetationBenchmarkResult, fetchVegetationShadowResult } = await server.ssrLoadModule( + ({ + fetchVegetationBenchmarkResult, + fetchVegetationShadowResult, + vegetationFullRouteMaskUrl, + } = await server.ssrLoadModule( "/src/core/laboratory/vegetationShadow.ts", )); }); @@ -148,6 +153,63 @@ function coarseRouteVideo() { }; } +function fullRouteReview() { + const layer = (kind) => ({ + name: kind === "city" ? "EoMT Cityscapes" : "ddrnet_39", + result_id: kind === "city" + ? `result-${"2".repeat(64)}` + : `lab-v1-ravnoves-video-ddrnet-${"3".repeat(64)}`, + frame_count: 6830, + taxonomy: { + schema_version: kind === "city" + ? "missioncore.recorded-eomt-taxonomy/v1" + : "missioncore.lab-v1-vegetation-taxonomy/v1", + classes: Array.from({ length: kind === "city" ? 16 : 64 }, (_, classId) => ({ + class_id: classId, + label: classId === 0 ? "undefined" : `${kind}-${classId}`, + color_rgb: [classId, classId, classId], + disposition: classId === 0 ? "undefined" : "prediction", + })), + }, + mask_archive: { + path: kind === "city" + ? "video/eomt-semantic-masks.zip" + : "video/ddrnet-semantic-masks.zip", + sha256: "4".repeat(64), + byte_length: 4096, + }, + inference_fps: 9.5, + latency_p95_ms: 101.2, + peak_reserved_vram_bytes: 3_000_000_000, + }); + return { + source_id: "RAVNOVES004TREE", + session_id: "20260828T130511Z_viewer_live", + source_job_id: "recorded-camera-eb2783c5480d56bda07c8af0", + source_job_input_sha256: "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715", + source_stream_sha256: "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac", + recorded_media_source_id: "recorded.camera.6a3945242828a038", + recorded_media_generation_sha256: "b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded", + frame_count: 6830, + width: 800, + height: 600, + timeline_start_seconds: 39.215263458, + timeline_end_seconds: 757.260263458, + frame_source_times_ns: Array.from({ length: 6830 }, (_, index) => 39_215_263_458 + index * 100_000_000), + ground_truth: false, + decode_repair: { + repaired_frame_count: 1, + sequence: 6092, + method: "duplicate-previous-decoded-frame", + proofs: { + eomt: { path: "proofs/decode_repair.json", sha256: "7".repeat(64) }, + ddrnet: { path: "proofs/ddrnet_decode_repair.json", sha256: "8".repeat(64) }, + }, + }, + layers: { city: layer("city"), vegetation: layer("vegetation") }, + }; +} + function labPayload(route = routeVideo()) { return { schema_version: "missioncore.lab-v1-vegetation-shadow/v1", @@ -233,6 +295,29 @@ test("vegetation LAB parses coarse material policy and sealed TGS binding", asyn assert.equal(result.routeVideo.fusionMode, "synchronised-multilayer-review"); }); +test("vegetation LAB parses the full 004 pass inside the existing result contract", async () => { + const payload = { + ...labPayload(null), + catalogs: { goose: [], ravnoves: [] }, + route_full_review: fullRouteReview(), + }; + const result = await fetchVegetationShadowResult(resultId, { + fetcher: async () => new Response(JSON.stringify(payload), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + }); + assert.equal(result.routeVideo, null); + assert.equal(result.routeFullReview.frameCount, 6830); + assert.equal(result.routeFullReview.city.taxonomy.length, 16); + assert.equal(result.routeFullReview.vegetation.taxonomy.length, 64); + assert.equal(result.routeFullReview.decodeRepair.sequence, 6092); + assert.equal( + vegetationFullRouteMaskUrl(resultId, "vegetation", 6829), + `/api/v1/laboratory/vegetation-shadow/${resultId}/route-masks/vegetation/6829`, + ); +}); + test("vegetation GOOSE benchmark opens through its separate archival endpoint", async () => { let requestedUrl = ""; const result = await fetchVegetationBenchmarkResult(benchmarkResultId, { @@ -271,8 +356,10 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr assert.match(resultSource, /M49TgsFullShadowEvidence/); assert.match(resultSource, /semanticOverride/); assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/); - assert.equal(resultSource.match(/&1 | ForEach-Object { Write-Output $_ } + $dockerExitCode = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($dockerExitCode -ne 0) { + throw "LAB V1 container failed with exit code $dockerExitCode" } } function Export-RavnovesFrames { param([string]$Destination) - Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $ravnovesSha256 + Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $RavnovesSha256 New-Item -ItemType Directory -Path $Destination | Out-Null $expression = ($frameIndices | ForEach-Object { "eq(n\,$_ )" }) -join "+" $temporaryPattern = Join-Path $Destination "selected-%03d.png" @@ -175,14 +234,68 @@ function Export-RavnovesFrames { function Export-RavnovesVideoFrames { param([string]$Destination) - Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $ravnovesSha256 + Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $RavnovesSha256 New-Item -ItemType Directory -Path $Destination | Out-Null - & ffmpeg -hide_banner -loglevel error -i $RavnovesVideo -map 0:v:0 -fps_mode passthrough (Join-Path $Destination "frame-%06d.png") + $decodedRoot = "{0}-decoded-by-pts" -f $Destination + $packetsPath = "{0}-packets.csv" -f $Destination + New-Item -ItemType Directory -Path $decodedRoot | Out-Null + & ffprobe -v error -select_streams v:0 -show_packets -show_entries packet=pts,flags -of csv=p=0 -o $packetsPath $RavnovesVideo + if ($LASTEXITCODE -ne 0) { + throw "RAVNOVES full-video packet probe failed" + } + $packetRows = @(Get-Content -LiteralPath $packetsPath | Select-Object -First $RavnovesExpectedFrameCount) + if ($packetRows.Count -ne $RavnovesExpectedFrameCount) { + throw "RAVNOVES full-video packet sequence changed" + } + & ffmpeg -hide_banner -loglevel error ` + -hwaccel cuda -hwaccel_output_format cuda -c:v h264_cuvid ` + -err_detect ignore_err -flags +output_corrupt -copyts ` + -i $RavnovesVideo -map 0:v:0 -vf "hwdownload,format=nv12" ` + -fps_mode passthrough -enc_time_base demux -frames:v $RavnovesExpectedFrameCount ` + -frame_pts 1 (Join-Path $decodedRoot "frame-%d.png") if ($LASTEXITCODE -ne 0) { throw "RAVNOVES full-video frame extraction failed" } + $decodedCount = @(Get-ChildItem -LiteralPath $decodedRoot -File -Filter "frame-*.png").Count + $repairs = @() + for ($index = 0; $index -lt $RavnovesExpectedFrameCount; $index++) { + $columns = ([string]$packetRows[$index]).Split(",") + if ($columns.Count -lt 2) { + throw "RAVNOVES full-video packet row is malformed" + } + $pts = [int64]::Parse($columns[0].Trim(), [Globalization.CultureInfo]::InvariantCulture) + $decodedPath = Join-Path $decodedRoot ("frame-{0}.png" -f $pts) + $canonicalPath = Join-Path $Destination ("frame-{0:D6}.png" -f ($index + 1)) + if (Test-Path -LiteralPath $decodedPath -PathType Leaf) { + Move-Item -LiteralPath $decodedPath -Destination $canonicalPath + continue + } + if ($index -eq 0 -or $repairs.Count -ge 1) { + throw "RAVNOVES source contains more than one recoverable decoder gap" + } + $previousPath = Join-Path $Destination ("frame-{0:D6}.png" -f $index) + Copy-Item -LiteralPath $previousPath -Destination $canonicalPath + $repairs += [ordered]@{ + sequence = $index + 1 + packet_pts = $pts + method = "duplicate-previous-decoded-frame" + } + } + Remove-Item -LiteralPath $decodedRoot -Recurse -Force + Remove-Item -LiteralPath $packetsPath -Force + [ordered]@{ + schema_version = "missioncore.recorded-video-decode-repair/v1" + decoder = "ffmpeg-h264_cuvid-output-corrupt" + packets_requested = $RavnovesExpectedFrameCount + frames_decoded = $decodedCount + repaired_frame_count = $repairs.Count + repairs = $repairs + } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath ( + Join-Path (Split-Path $Destination -Parent) "decode-repair.json" + ) -Encoding utf8 $frames = @(Get-ChildItem -LiteralPath $Destination -File -Filter "frame-*.png" | Sort-Object Name) - if ($frames.Count -ne 4489 -or $frames[0].Name -ne "frame-000001.png" -or $frames[-1].Name -ne "frame-004489.png") { + $lastFrameName = "frame-{0:D6}.png" -f $RavnovesExpectedFrameCount + if ($frames.Count -ne $RavnovesExpectedFrameCount -or $frames[0].Name -ne "frame-000001.png" -or $frames[-1].Name -ne $lastFrameName) { throw "RAVNOVES full-video frame sequence changed" } } @@ -244,6 +357,9 @@ try { $framesRoot = Join-Path $runRoot "input-frames" Export-RavnovesVideoFrames -Destination $framesRoot Invoke-IsolatedRun -RunMode "ravnoves-video" -RunRoot $runRoot -Limit 0 -FramesRoot $framesRoot + Copy-Item -LiteralPath (Join-Path $runRoot "decode-repair.json") -Destination ( + Join-Path $runRoot "result\decode-repair.json" + ) Remove-Item -LiteralPath $framesRoot -Recurse -Force } } diff --git a/scripts/seal_mixed_route_full_video_review.py b/scripts/seal_mixed_route_full_video_review.py new file mode 100644 index 0000000..1f67a71 --- /dev/null +++ b/scripts/seal_mixed_route_full_video_review.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Seal the complete RAVNOVES004TREE semantic pass into existing LAB V1.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from k1link.laboratory.mixed_route_vegetation_review import ( + seal_mixed_route_full_video_review, +) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--base-lab-root", type=Path, required=True) + parser.add_argument("--job-root", type=Path, required=True) + parser.add_argument("--recorded-media-preparation", type=Path, required=True) + parser.add_argument("--eomt-root", type=Path, required=True) + parser.add_argument("--eomt-profile", type=Path, required=True) + parser.add_argument("--ddrnet-root", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + print( + seal_mixed_route_full_video_review( + base_lab_root=args.base_lab_root, + job_root=args.job_root, + recorded_media_preparation_path=args.recorded_media_preparation, + eomt_root=args.eomt_root, + eomt_profile_path=args.eomt_profile, + ddrnet_root=args.ddrnet_root, + output_root=args.output_root, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/src/k1link/laboratory/mixed_route_vegetation_review.py b/src/k1link/laboratory/mixed_route_vegetation_review.py index ec30939..fc67c36 100644 --- a/src/k1link/laboratory/mixed_route_vegetation_review.py +++ b/src/k1link/laboratory/mixed_route_vegetation_review.py @@ -7,7 +7,9 @@ import hashlib import json import os import shutil +import tarfile import tempfile +import zipfile from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -15,6 +17,8 @@ from typing import Any import numpy as np from PIL import Image, ImageDraw +from k1link.compute.jobs import validate_camera_compute_job + from k1link.laboratory.vegetation_shadow_lab import ( LAB_SCHEMA, RESULT_PREFIX, @@ -45,11 +49,20 @@ TGS_COLORS = { 2: (235, 112, 122), 3: (150, 154, 163), } +FULL_ROUTE_SOURCE_ID = "RAVNOVES004TREE" +FULL_ROUTE_FRAME_COUNT = 6830 +FULL_ROUTE_JOB_ID = "recorded-camera-eb2783c5480d56bda07c8af0" +FULL_ROUTE_INPUT_SHA256 = ( + "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715" +) +FULL_ROUTE_STREAM_SHA256 = ( + "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac" +) def _read_json(path: Path, label: str) -> dict[str, Any]: try: - value = json.loads(path.read_text(encoding="utf-8")) + value = json.loads(path.read_text(encoding="utf-8-sig")) except (OSError, json.JSONDecodeError) as exc: raise VegetationShadowLabError(f"{label} is invalid") from exc if not isinstance(value, dict): @@ -86,6 +99,126 @@ def _image_proof(descriptor: dict[str, object]) -> dict[str, object]: return {"path": descriptor["path"], "sha256": descriptor["sha256"]} +def _mask_archive_descriptor( + path: Path, + relative: str, + artifacts: list[dict[str, object]], + *, + role: str, +) -> dict[str, object]: + descriptor = { + "role": role, + "path": relative, + "byte_length": path.stat().st_size, + "sha256": sha256_path(path), + "media_type": "application/zip", + } + artifacts.append(descriptor) + return descriptor + + +def _repack_eomt_masks(source: Path, destination: Path, frame_count: int) -> None: + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + expected = [f"semantic-masks/frame-{sequence + 1:06d}.png" for sequence in range(frame_count)] + try: + with ( + tarfile.open(source, mode="r:gz") as archive, + zipfile.ZipFile( + destination, + mode="x", + compression=zipfile.ZIP_STORED, + allowZip64=True, + ) as output, + ): + members = [member for member in archive.getmembers() if member.isfile()] + if [member.name.removeprefix("./") for member in members] != expected: + raise VegetationShadowLabError("full-route EoMT mask sequence changed") + for member, expected_name in zip(members, expected, strict=True): + if member.size < 8 or member.size > 1024 * 1024: + raise VegetationShadowLabError("full-route EoMT mask size changed") + stream = archive.extractfile(member) + if stream is None: + raise VegetationShadowLabError("full-route EoMT mask is unavailable") + output.writestr( + f"masks/{Path(expected_name).name}", + stream.read(), + ) + except (OSError, tarfile.TarError, zipfile.BadZipFile) as exc: + destination.unlink(missing_ok=True) + raise VegetationShadowLabError("full-route EoMT archive is invalid") from exc + + +def _validate_zip_masks(path: Path, frame_count: int) -> None: + expected = [f"masks/frame-{sequence + 1:06d}.png" for sequence in range(frame_count)] + try: + with zipfile.ZipFile(path) as archive: + members = archive.infolist() + if ( + [member.filename for member in members] != expected + or any( + member.is_dir() or member.file_size < 8 or member.file_size > 1024 * 1024 + for member in members + ) + ): + raise VegetationShadowLabError("full-route semantic mask sequence changed") + except (OSError, zipfile.BadZipFile) as exc: + raise VegetationShadowLabError("full-route semantic archive is invalid") from exc + + +def _full_route_frame_times(media: dict[str, Any], frame_count: int) -> list[int]: + epochs = media.get("epochs") + start = media.get("timeline_start_seconds") + end = media.get("timeline_end_seconds") + if ( + not isinstance(epochs, list) + or len(epochs) != 1 + or not isinstance(start, (int, float)) + or not isinstance(end, (int, float)) + ): + raise VegetationShadowLabError("recorded media timeline changed") + epoch = epochs[0] + segments = epoch.get("segments") if isinstance(epoch, dict) else None + if not isinstance(segments, list) or len(segments) != frame_count: + raise VegetationShadowLabError("recorded media segment count changed") + starts = [float(start)] + previous_end = 0.0 + for sequence, raw in enumerate(segments, start=1): + if ( + not isinstance(raw, dict) + or raw.get("sequence") != sequence + or not isinstance(raw.get("end_time_seconds"), (int, float)) + or float(raw["end_time_seconds"]) <= previous_end + ): + raise VegetationShadowLabError("recorded media segment timeline changed") + if sequence < frame_count: + starts.append(float(start) + float(raw["end_time_seconds"])) + previous_end = float(raw["end_time_seconds"]) + if abs((float(start) + previous_end) - float(end)) > 0.001: + raise VegetationShadowLabError("recorded media duration changed") + return [round(value * 1_000_000_000) for value in starts] + + +def _eomt_taxonomy(profile: dict[str, Any]) -> dict[str, object]: + taxonomy = profile.get("target_taxonomy") + if not isinstance(taxonomy, dict) or set(taxonomy) != {str(index) for index in range(16)}: + raise VegetationShadowLabError("EoMT target taxonomy changed") + classes = [] + for class_id in range(16): + digest = hashlib.sha256(f"mission-core-segment-{class_id}".encode()).digest() + classes.append( + { + "class_id": class_id, + "label": taxonomy[str(class_id)], + "color_rgb": [64 + digest[index] % 176 for index in range(3)], + "disposition": "undefined" if class_id == 0 else "prediction", + } + ) + return { + "schema_version": "missioncore.recorded-eomt-taxonomy/v1", + "classes": classes, + } + + def _render_tgs_costmaps(tgs_root: Path, destination: Path) -> list[Path]: result = _read_json(tgs_root / "result.json", "mixed-route TGS result") evidence = result.get("evidence") @@ -392,6 +525,338 @@ def seal_mixed_route_vegetation_review( raise +def seal_mixed_route_full_video_review( + *, + base_lab_root: Path, + job_root: Path, + recorded_media_preparation_path: Path, + eomt_root: Path, + eomt_profile_path: Path, + ddrnet_root: Path, + output_root: Path, +) -> Path: + """Publish the complete 004 city/nature pass in the existing M4.7 LAB.""" + + base_root = base_lab_root.resolve(strict=True) + base = _read_json(base_root / "result.json", "base vegetation LAB") + base_identity = base.get("identity") + if ( + base.get("schema_version") != LAB_SCHEMA + or not isinstance(base_identity, dict) + or hashlib.sha256(canonical_json(base_identity)).hexdigest() + != base.get("identity_sha256") + or base.get("result_id") != base_root.name + or not base_root.name.startswith(RESULT_PREFIX) + or base.get("authority", {}).get("commands_enabled") is not False + ): + raise VegetationShadowLabError("base vegetation LAB proof changed") + + job = validate_camera_compute_job(job_root) + if ( + job.job_id != FULL_ROUTE_JOB_ID + or job.input_sha256 != FULL_ROUTE_INPUT_SHA256 + or job.session_id != "20260828T130511Z_viewer_live" + or job.source_id != "sensor.camera.right" + or job.segment_count != FULL_ROUTE_FRAME_COUNT + ): + raise VegetationShadowLabError("full-route camera job changed") + + eomt = _read_json(eomt_root / "result.json", "full-route EoMT result") + eomt_report = _read_json(eomt_root / "run-report.json", "full-route EoMT report") + decode_repair = _read_json( + eomt_root / "decode-repair.json", + "full-route video decode repair", + ) + eomt_input = eomt_report.get("input") + eomt_metrics = eomt_report.get("metrics") + if ( + eomt.get("schema_version") != "missioncore.recorded-perception-result/v2" + or eomt.get("ground_truth") is not False + or eomt.get("frames_processed") != FULL_ROUTE_FRAME_COUNT + or not isinstance(eomt_input, dict) + or eomt_input.get("job_id") != job.job_id + or eomt_input.get("input_sha256") != job.input_sha256 + or eomt_input.get("frames_admitted") != FULL_ROUTE_FRAME_COUNT + or not isinstance(eomt_metrics, dict) + or eomt_metrics.get("frames_processed") != FULL_ROUTE_FRAME_COUNT + ): + raise VegetationShadowLabError("full-route EoMT contract changed") + if ( + decode_repair.get("schema_version") + != "missioncore.recorded-video-decode-repair/v1" + or decode_repair.get("decoder") != "ffmpeg-h264_cuvid-output-corrupt" + or decode_repair.get("packets_requested") != FULL_ROUTE_FRAME_COUNT + or decode_repair.get("frames_decoded") != FULL_ROUTE_FRAME_COUNT - 1 + or decode_repair.get("repaired_frame_count") != 1 + or decode_repair.get("repairs") + != [ + { + "sequence": 6092, + "packet_pts": 55656450, + "method": "duplicate-previous-decoded-frame", + } + ] + ): + raise VegetationShadowLabError("full-route video decode repair changed") + eomt_artifacts = { + item.get("kind"): item + for item in eomt.get("artifacts", []) + if isinstance(item, dict) + } + eomt_archive_proof = eomt_artifacts.get("panoptic-mask-archive") + if not isinstance(eomt_archive_proof, dict): + raise VegetationShadowLabError("full-route EoMT mask proof is missing") + eomt_archive = eomt_root / str(eomt_archive_proof.get("path")) + if ( + not eomt_archive.is_file() + or eomt_archive.stat().st_size != eomt_archive_proof.get("byte_length") + or sha256_path(eomt_archive) != eomt_archive_proof.get("sha256") + ): + raise VegetationShadowLabError("full-route EoMT mask proof changed") + + ddrnet = _read_json(ddrnet_root / "result.json", "full-route DDRNet result") + ddrnet_decode_repair = _read_json( + ddrnet_root / "decode-repair.json", + "full-route DDRNet video decode repair", + ) + ddrnet_source = ddrnet.get("source") + ddrnet_video = ddrnet.get("video_semantics") + if ( + ddrnet.get("schema_version") != "missioncore.lab-v1-goose-vegetation-run/v1" + or ddrnet.get("mode") != "ravnoves-video" + or ddrnet.get("candidate", {}).get("candidate_key") != "ddrnet" + or not isinstance(ddrnet_source, dict) + or ddrnet_source.get("source_id") + != f"{FULL_ROUTE_SOURCE_ID}/right-{FULL_ROUTE_STREAM_SHA256}" + or ddrnet_source.get("input_count") != FULL_ROUTE_FRAME_COUNT + or ddrnet_source.get("ground_truth_available") is not False + or not isinstance(ddrnet_video, dict) + or ddrnet_video.get("base_m4_result_id") is not None + or ddrnet.get("authority", {}).get("navigation_accepted") is not False + or ddrnet.get("authority", {}).get("actuation_accepted") is not False + ): + raise VegetationShadowLabError("full-route DDRNet contract changed") + if ddrnet_decode_repair != decode_repair: + raise VegetationShadowLabError("full-route model decoders disagree") + ddrnet_archive_proof = ddrnet_video.get("mask_archive") + ddrnet_taxonomy = ddrnet_video.get("taxonomy") + if ( + not isinstance(ddrnet_archive_proof, dict) + or ddrnet_archive_proof.get("frame_count") != FULL_ROUTE_FRAME_COUNT + or not isinstance(ddrnet_taxonomy, dict) + ): + raise VegetationShadowLabError("full-route DDRNet mask proof changed") + ddrnet_archive = ddrnet_root / str(ddrnet_archive_proof.get("path")) + if ( + not ddrnet_archive.is_file() + or ddrnet_archive.stat().st_size != ddrnet_archive_proof.get("byte_length") + or sha256_path(ddrnet_archive) != ddrnet_archive_proof.get("sha256") + ): + raise VegetationShadowLabError("full-route DDRNet archive changed") + _validate_zip_masks(ddrnet_archive, FULL_ROUTE_FRAME_COUNT) + + media_document = _read_json( + recorded_media_preparation_path.resolve(strict=True), + "recorded media preparation", + ) + media = media_document.get("manifest") + if ( + media_document.get("schema_version") != "missioncore.recorded-media-preparation/v3" + or media_document.get("session_id") != job.session_id + or media_document.get("artifact_id") != "recorded-video-6a3945242828a038" + or media_document.get("checksum_sha256") + != "557e61f2839140dc9f97b5aea855c576b0616573080dff5d2852ab1df0558665" + or not isinstance(media, dict) + or media.get("source_id") != "recorded.camera.6a3945242828a038" + or media.get("generation_sha256") + != "b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded" + or media.get("byte_length") != 551674491 + or media.get("timeline_start_seconds") != job.timeline_start_seconds + or media.get("timeline_end_seconds") != job.timeline_end_seconds + or media.get("synchronization") != "host-arrival-best-effort" + ): + raise VegetationShadowLabError("recorded media preparation changed") + frame_times_ns = _full_route_frame_times(media, FULL_ROUTE_FRAME_COUNT) + eomt_profile = _read_json(eomt_profile_path.resolve(strict=True), "EoMT profile") + eomt_taxonomy = _eomt_taxonomy(eomt_profile) + + output_root.mkdir(mode=0o700, parents=True, exist_ok=True) + temporary = Path(tempfile.mkdtemp(prefix=".mixed-route-full-video-", dir=output_root)) + artifacts: list[dict[str, object]] = [] + try: + eomt_destination = temporary / "video" / "eomt-semantic-masks.zip" + _repack_eomt_masks(eomt_archive, eomt_destination, FULL_ROUTE_FRAME_COUNT) + _validate_zip_masks(eomt_destination, FULL_ROUTE_FRAME_COUNT) + eomt_descriptor = _mask_archive_descriptor( + eomt_destination, + "video/eomt-semantic-masks.zip", + artifacts, + role="full-route-eomt-semantic-mask-archive", + ) + ddrnet_descriptor = _artifact( + ddrnet_archive, + temporary, + "video/ddrnet-semantic-masks.zip", + artifacts, + role="full-route-ddrnet-semantic-mask-archive", + media_type="application/zip", + ) + _validate_zip_masks( + temporary / "video" / "ddrnet-semantic-masks.zip", + FULL_ROUTE_FRAME_COUNT, + ) + proof_descriptors: dict[str, dict[str, object]] = {} + for key, path in ( + ("base", base_root / "result.json"), + ("job", job.manifest_path), + ("media", recorded_media_preparation_path.resolve(strict=True)), + ("eomt", eomt_root / "result.json"), + ("eomt_report", eomt_root / "run-report.json"), + ("decode_repair", eomt_root / "decode-repair.json"), + ("ddrnet", ddrnet_root / "result.json"), + ("ddrnet_decode_repair", ddrnet_root / "decode-repair.json"), + ): + descriptor = _artifact( + path, + temporary, + f"proofs/{key}.json", + artifacts, + role="full-route-proof", + media_type="application/json", + ) + proof_descriptors[key] = _image_proof(descriptor) + + full_route = { + "source_id": FULL_ROUTE_SOURCE_ID, + "session_id": job.session_id, + "source_job_id": job.job_id, + "source_job_input_sha256": job.input_sha256, + "source_stream_sha256": FULL_ROUTE_STREAM_SHA256, + "recorded_media_source_id": media["source_id"], + "recorded_media_generation_sha256": media["generation_sha256"], + "frame_count": FULL_ROUTE_FRAME_COUNT, + "width": 800, + "height": 600, + "timeline_start_seconds": job.timeline_start_seconds, + "timeline_end_seconds": job.timeline_end_seconds, + "frame_source_times_ns": frame_times_ns, + "ground_truth": False, + "decode_repair": { + "repaired_frame_count": 1, + "sequence": 6092, + "method": "duplicate-previous-decoded-frame", + "proofs": { + "eomt": proof_descriptors["decode_repair"], + "ddrnet": proof_descriptors["ddrnet_decode_repair"], + }, + }, + "layers": { + "city": { + "name": "EoMT Cityscapes", + "result_id": eomt["result_id"], + "frame_count": FULL_ROUTE_FRAME_COUNT, + "taxonomy": eomt_taxonomy, + "mask_archive": { + "path": eomt_descriptor["path"], + "sha256": eomt_descriptor["sha256"], + "byte_length": eomt_descriptor["byte_length"], + }, + "inference_fps": eomt_metrics["inference_frames_per_second"], + "latency_p95_ms": eomt_metrics["latency_ms"]["end_to_end_ms"]["p95"], + "peak_reserved_vram_bytes": int( + float(eomt_metrics["cuda_peak_memory_reserved_mib"]) * 1024 * 1024 + ), + }, + "vegetation": { + "name": ddrnet["candidate"]["loaded_model_name"], + "result_id": ddrnet["result_id"], + "frame_count": FULL_ROUTE_FRAME_COUNT, + "taxonomy": ddrnet_taxonomy, + "mask_archive": { + "path": ddrnet_descriptor["path"], + "sha256": ddrnet_descriptor["sha256"], + "byte_length": ddrnet_descriptor["byte_length"], + }, + "inference_fps": ddrnet["timing"]["throughput_fps_from_mean_inference"], + "latency_p95_ms": ddrnet["timing"]["latency_ms_p95"], + "peak_reserved_vram_bytes": ddrnet["resource"]["peak_reserved_vram_bytes"], + }, + }, + "proofs": proof_descriptors, + "limitations": [ + "RAVNOVES004TREE has no manual route truth.", + "One corrupt H.264 packet at sequence 6092 was represented by the previous decoded frame; the repair is sealed as evidence.", + "EoMT and DDRNet were executed sequentially, not as a concurrent realtime stack.", + "DDRNet vegetation subtypes remain prediction-only and are not planner authority.", + "This full-video pass does not add full-route TGS, ditch or negative-obstacle proof.", + "People and vehicles still require an independent fail-safe detector and STOP path.", + ], + } + authority = { + "commands_enabled": False, + "navigation_or_safety_accepted": False, + "actuation_accepted": False, + "camera_semantics_can_clear_rigid_geometry": False, + } + identity = { + "lab_id": "lab-v1-vegetation-mission-policy", + "base_result_id": base["result_id"], + "selected_candidate": base_identity["selected_candidate"], + "candidate_metrics": base_identity["candidate_metrics"], + "source": { + "shadow_session": FULL_ROUTE_SOURCE_ID, + "shadow_camera": job.source_id, + "shadow_frame_count": FULL_ROUTE_FRAME_COUNT, + "video_shadow_frame_count": FULL_ROUTE_FRAME_COUNT, + }, + "route_full_review": full_route, + "authority": authority, + } + identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest() + result_id = f"{RESULT_PREFIX}{identity_sha256}" + manifest = { + "schema_version": LAB_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": datetime.now(UTC).isoformat(), + "ground_truth": False, + "status": "visual-shadow-ready-policy-not-authorized", + "identity": identity, + "source": identity["source"], + "route_video": None, + "route_review": None, + "route_full_review": full_route, + "method": { + "completeness": "complete", + "execution_class": "ai-inference", + "pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1", + }, + "metrics": {"candidates": base["metrics"]["candidates"]}, + "decision": { + "selected_candidate": base_identity["selected_candidate"], + "visual_shadow_ready": True, + "full_video_shadow_ready": True, + "mission_policy_ready_for_configuration": True, + "multilayer_policy_review_ready": True, + "navigation_accepted": False, + "production_accepted": False, + }, + "limitations": full_route["limitations"], + "authority": authority, + "catalogs": {"goose": [], "ravnoves": []}, + "artifacts": artifacts, + } + (temporary / "result.json").write_bytes(canonical_json(manifest) + b"\n") + destination = output_root / result_id + if destination.exists(): + raise VegetationShadowLabError("immutable full-route LAB result already exists") + os.replace(temporary, destination) + return destination + except Exception: + shutil.rmtree(temporary, ignore_errors=True) + raise + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--base-lab-root", type=Path, required=True) diff --git a/src/k1link/web/vegetation_shadow_lab_api.py b/src/k1link/web/vegetation_shadow_lab_api.py index b459968..dbb3be7 100644 --- a/src/k1link/web/vegetation_shadow_lab_api.py +++ b/src/k1link/web/vegetation_shadow_lab_api.py @@ -179,9 +179,77 @@ def _build_vegetation_lab_router( }, ) + @router.get("/{result_id}/route-masks/{layer}/{sequence}") + def get_full_route_mask(result_id: str, layer: str, sequence: int) -> Response: + candidate = _resolve_candidate(root_provider, definition, result_id) + manifest = _read_verified(candidate, definition) + route = manifest.get("route_full_review") + layers = route.get("layers") if isinstance(route, dict) else None + frame_count = route.get("frame_count") if isinstance(route, dict) else None + selected = layers.get(layer) if isinstance(layers, dict) else None + archive = selected.get("mask_archive") if isinstance(selected, dict) else None + archive_relative = archive.get("path") if isinstance(archive, dict) else None + if ( + layer not in {"city", "vegetation"} + or not isinstance(frame_count, int) + or not 0 <= sequence < frame_count + or not isinstance(archive_relative, str) + ): + raise HTTPException(status_code=404, detail="Full-route semantic mask not found") + relative = PurePosixPath(archive_relative) + artifacts = manifest.get("artifacts") + if ( + relative.is_absolute() + or str(relative) != archive_relative + or any(part in {"", ".", ".."} for part in relative.parts) + or relative.suffix != ".zip" + or not isinstance(artifacts, list) + or not any( + isinstance(item, dict) + and item.get("path") == archive_relative + and item.get("media_type") == "application/zip" + for item in artifacts + ) + ): + raise HTTPException(status_code=404, detail="Full-route semantic mask not found") + return _zip_mask_response(candidate.joinpath(*relative.parts), sequence) + return router +def _zip_mask_response(archive_path: Path, sequence: int) -> Response: + member = f"masks/frame-{sequence + 1:06d}.png" + try: + before = archive_path.stat() + with zipfile.ZipFile(archive_path) as frozen: + info = frozen.getinfo(member) + if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024: + raise ValueError("Semantic mask member is invalid") + payload = frozen.read(info) + after = archive_path.stat() + if ( + before.st_size != after.st_size + or before.st_mtime_ns != after.st_mtime_ns + or len(payload) != info.file_size + ): + raise ValueError("Semantic mask archive changed during read") + except (KeyError, OSError, ValueError, zipfile.BadZipFile): + raise HTTPException( + status_code=503, + detail="Semantic mask failed verification", + ) from None + digest = hashlib.sha256(payload).hexdigest() + return Response( + content=payload, + media_type="image/png", + headers={ + "Cache-Control": "private, max-age=31536000, immutable", + "ETag": f'"{digest}"', + "X-Content-Type-Options": "nosniff", + }, + ) + + def _configured_root(provider: RootProvider) -> Path | None: candidate = provider() if candidate is None: diff --git a/tests/test_e4_full_session_segmentation.py b/tests/test_e4_full_session_segmentation.py index 7b2d911..ab9a605 100644 --- a/tests/test_e4_full_session_segmentation.py +++ b/tests/test_e4_full_session_segmentation.py @@ -104,3 +104,20 @@ def test_e4_class_fractions_use_only_valid_fov_pixels() -> None: assert {item["id"]: item["pixels"] for item in classes} == {1: 1, 4: 2, 7: 1} assert sum(float(item["fraction_of_valid_fov"]) for item in classes) == 1.0 + + +def test_e4_orchestrator_seals_a_single_decoder_gap_without_frame_shift() -> None: + path = ( + Path(__file__).parents[1] + / "experiments" + / "perception" + / "worker" + / "Invoke-E4FullSessionSegmentation.ps1" + ) + source = path.read_text(encoding="utf-8") + assert "-c:v h264_cuvid" in source + assert "-frame_pts 1" in source + assert '$decodedPath = Join-Path $decodedFramesRoot ("frame-{0}.png" -f $pts)' in source + assert "$repairs.Count -ge 1" in source + assert 'method = "duplicate-previous-decoded-frame"' in source + assert 'schema_version = "missioncore.recorded-video-decode-repair/v1"' in source diff --git a/tests/test_lab_v1_goose_vegetation_benchmark.py b/tests/test_lab_v1_goose_vegetation_benchmark.py index 00bc275..fe59cde 100644 --- a/tests/test_lab_v1_goose_vegetation_benchmark.py +++ b/tests/test_lab_v1_goose_vegetation_benchmark.py @@ -25,6 +25,12 @@ POWERSHELL_PATH = ( / "worker" / "Invoke-LabV1VegetationGooseBenchmark.ps1" ) +RAV004_SOURCE_PATH = ( + REPOSITORY_ROOT + / "config" + / "perception" + / "lab-v1-ravnoves004tree-full-video-source-v1.json" +) def test_benchmark_contract_is_bounded_and_fail_closed() -> None: @@ -93,3 +99,21 @@ def test_worker_wrapper_is_isolated_from_canonical_triton() -> None: assert '"--cap-drop", "ALL"' in source assert '"--security-opt", "no-new-privileges"' in source assert "if ($canonicalAfter -ne $canonicalBefore)" in source + + +def test_rav004_full_video_profile_and_decoder_gap_are_explicit() -> None: + profile = json.loads(RAV004_SOURCE_PATH.read_text(encoding="utf-8")) + source_profile = profile["source"] + assert profile["schema_version"] == "missioncore.lab-v1-ravnoves-source/v1" + assert source_profile["source_job_id"] == ( + "recorded-camera-eb2783c5480d56bda07c8af0" + ) + assert source_profile["expected_frame_count"] == 6830 + assert source_profile["base_m4_result_id"] is None + source = POWERSHELL_PATH.read_text(encoding="utf-8") + assert "-c:v h264_cuvid" in source + assert 'schema_version = "missioncore.recorded-video-decode-repair/v1"' in source + assert 'method = "duplicate-previous-decoded-frame"' in source + assert "$repairs.Count -ge 1" in source + assert '& docker @arguments 2>&1 | ForEach-Object { Write-Output $_ }' in source + assert 'if ($dockerExitCode -ne 0)' in source diff --git a/tests/test_vegetation_shadow_lab.py b/tests/test_vegetation_shadow_lab.py index 7d465bc..f986067 100644 --- a/tests/test_vegetation_shadow_lab.py +++ b/tests/test_vegetation_shadow_lab.py @@ -270,6 +270,67 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence( assert mask.content == b"\x89PNG\r\n\x1a\n" assert mask.headers["cache-control"].endswith("immutable") + full_archive_payloads = (b"\x89PNG\r\n\x1a\ncity", b"\x89PNG\r\n\x1a\nvegetation") + full_identity = dict(manifest["identity"]) + full_route = { + "frame_count": 2, + "layers": { + layer: {"mask_archive": {"path": "video/full-route-masks.zip"}} + for layer in ("city", "vegetation") + }, + } + full_identity["route_full_review"] = full_route + full_identity_sha = hashlib.sha256( + json.dumps( + full_identity, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).hexdigest() + full_result_id = f"lab-v1-vegetation-shadow-{full_identity_sha}" + full_root = result_root.parent / full_result_id + shutil.copytree(result_root, full_root) + full_archive = full_root / "video" / "full-route-masks.zip" + full_archive.parent.mkdir(exist_ok=True) + with zipfile.ZipFile(full_archive, "x", compression=zipfile.ZIP_STORED) as frozen: + for sequence, payload in enumerate(full_archive_payloads, start=1): + frozen.writestr(f"masks/frame-{sequence:06d}.png", payload) + full_manifest = dict(manifest) + full_manifest["result_id"] = full_result_id + full_manifest["identity"] = full_identity + full_manifest["identity_sha256"] = full_identity_sha + full_manifest["route_full_review"] = full_route + full_manifest["artifacts"] = [ + *manifest["artifacts"], + { + "role": "full-route-mask-fixture", + "path": "video/full-route-masks.zip", + "byte_length": full_archive.stat().st_size, + "sha256": _sha256(full_archive), + "media_type": "application/zip", + }, + ] + (full_root / "result.json").write_text( + json.dumps(full_manifest, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + "\n", + encoding="utf-8", + ) + for layer, sequence, expected in ( + ("city", 0, full_archive_payloads[0]), + ("vegetation", 1, full_archive_payloads[1]), + ): + response = client.get( + f"/api/v1/laboratory/vegetation-shadow/{full_result_id}" + f"/route-masks/{layer}/{sequence}" + ) + assert response.status_code == 200 + assert response.content == expected + assert response.headers["cache-control"].endswith("immutable") + assert client.get( + f"/api/v1/laboratory/vegetation-shadow/{full_result_id}/route-masks/city/2" + ).status_code == 404 + (result_root / asset_path).write_bytes(b"tampered") assert ( client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}").status_code From cd16ea28a658c55c2f69321c89a668dc7a2c7a99 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 29 Aug 2026 13:54:10 +0300 Subject: [PATCH 4/6] fix(lab): keep full route result indexable --- .../src/core/laboratory/vegetationShadow.ts | 85 +++++++++++++++---- .../test/vegetationShadow.test.mjs | 34 ++++++-- .../mixed_route_vegetation_review.py | 21 ++++- src/k1link/web/vegetation_shadow_lab_api.py | 48 +++++++++++ tests/test_vegetation_shadow_lab.py | 24 ++++++ 5 files changed, 189 insertions(+), 23 deletions(-) diff --git a/apps/control-station/src/core/laboratory/vegetationShadow.ts b/apps/control-station/src/core/laboratory/vegetationShadow.ts index 0b7cf5d..380ad58 100644 --- a/apps/control-station/src/core/laboratory/vegetationShadow.ts +++ b/apps/control-station/src/core/laboratory/vegetationShadow.ts @@ -129,6 +129,10 @@ export interface VegetationFullRouteReview { height: 600; timelineStartSeconds: number; timelineEndSeconds: number; + timelineArtifact: { + sha256: string; + byteLength: number; + }; frameSourceTimesNs: readonly number[]; decodeRepair: { repairedFrameCount: 1; @@ -742,22 +746,30 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null if (timelineEndSeconds <= timelineStartSeconds) { throw new VegetationShadowContractError("vegetation.route_full_review: timeline invalid."); } - const frameSourceTimesNs = arrayValue( - row.frame_source_times_ns, - "vegetation.route_full_review.frame_source_times_ns", - ).map((raw, index) => { - const time = integerValue(raw, `vegetation.route_full_review.frame_source_times_ns[${index}]`); - if (!Number.isSafeInteger(time)) { - throw new VegetationShadowContractError("vegetation.route_full_review: unsafe frame time."); - } - return time; - }); - if ( - frameSourceTimesNs.length !== 6830 - || frameSourceTimesNs.some((time, index) => index > 0 && time <= frameSourceTimesNs[index - 1]!) - ) { - throw new VegetationShadowContractError("vegetation.route_full_review: frame timeline changed."); + const timeline = objectValue(row.timeline, "vegetation.route_full_review.timeline"); + exact( + timeline.path, + "video/frame-source-times-ns.bin", + "vegetation.route_full_review.timeline.path", + ); + exact( + timeline.encoding, + "uint64-le-nanoseconds", + "vegetation.route_full_review.timeline.encoding", + ); + exact(timeline.frame_count, 6830, "vegetation.route_full_review.timeline.frame_count"); + const timelineSha256 = textValue( + timeline.sha256, + "vegetation.route_full_review.timeline.sha256", + ); + if (!SHA256.test(timelineSha256)) { + throw new VegetationShadowContractError("vegetation.route_full_review: timeline digest invalid."); } + const timelineByteLength = integerValue( + timeline.byte_length, + "vegetation.route_full_review.timeline.byte_length", + ); + exact(timelineByteLength, 6830 * 8, "vegetation.route_full_review.timeline.byte_length"); const decodeRepair = objectValue( row.decode_repair, "vegetation.route_full_review.decode_repair", @@ -808,7 +820,8 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null height: 600, timelineStartSeconds, timelineEndSeconds, - frameSourceTimesNs, + timelineArtifact: { sha256: timelineSha256, byteLength: timelineByteLength }, + frameSourceTimesNs: [], decodeRepair: { repairedFrameCount: 1, sequence: 6092, @@ -935,11 +948,49 @@ export async function fetchVegetationShadowResult( if (!response.ok) { throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`); } - return parseResult( + const result = parseResult( await response.json(), resultId, "/api/v1/laboratory/vegetation-shadow", ); + if (!result.routeFullReview) return result; + const timelineResponse = await fetcher( + `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-timeline`, + { method: "GET", headers: { Accept: "application/octet-stream" }, signal }, + ); + if (!timelineResponse.ok) { + throw new VegetationShadowContractError( + `Vegetation LAB timeline недоступна: HTTP ${timelineResponse.status}.`, + ); + } + if ( + timelineResponse.headers.get("etag") + !== `"${result.routeFullReview.timelineArtifact.sha256}"` + ) { + throw new VegetationShadowContractError("Vegetation LAB timeline digest изменён."); + } + const timelinePayload = await timelineResponse.arrayBuffer(); + if (timelinePayload.byteLength !== result.routeFullReview.timelineArtifact.byteLength) { + throw new VegetationShadowContractError("Vegetation LAB timeline size изменён."); + } + const timelineView = new DataView(timelinePayload); + const frameSourceTimesNs = Array.from({ length: result.routeFullReview.frameCount }, (_, index) => { + const value = Number(timelineView.getBigUint64(index * 8, true)); + if (!Number.isSafeInteger(value)) { + throw new VegetationShadowContractError("Vegetation LAB timeline содержит unsafe time."); + } + return value; + }); + if ( + frameSourceTimesNs[0] !== Math.round(result.routeFullReview.timelineStartSeconds * 1_000_000_000) + || frameSourceTimesNs.some((time, index) => index > 0 && time <= frameSourceTimesNs[index - 1]!) + ) { + throw new VegetationShadowContractError("Vegetation LAB timeline нарушена."); + } + return { + ...result, + routeFullReview: { ...result.routeFullReview, frameSourceTimesNs }, + }; } export async function fetchVegetationBenchmarkResult( diff --git a/apps/control-station/test/vegetationShadow.test.mjs b/apps/control-station/test/vegetationShadow.test.mjs index 5c61cb7..d4c96ba 100644 --- a/apps/control-station/test/vegetationShadow.test.mjs +++ b/apps/control-station/test/vegetationShadow.test.mjs @@ -195,7 +195,13 @@ function fullRouteReview() { height: 600, timeline_start_seconds: 39.215263458, timeline_end_seconds: 757.260263458, - frame_source_times_ns: Array.from({ length: 6830 }, (_, index) => 39_215_263_458 + index * 100_000_000), + timeline: { + path: "video/frame-source-times-ns.bin", + sha256: "5".repeat(64), + byte_length: 6830 * 8, + encoding: "uint64-le-nanoseconds", + frame_count: 6830, + }, ground_truth: false, decode_repair: { repaired_frame_count: 1, @@ -301,17 +307,35 @@ test("vegetation LAB parses the full 004 pass inside the existing result contrac catalogs: { goose: [], ravnoves: [] }, route_full_review: fullRouteReview(), }; + const timeline = new ArrayBuffer(6830 * 8); + const timelineView = new DataView(timeline); + for (let index = 0; index < 6830; index += 1) { + timelineView.setBigUint64( + index * 8, + BigInt(39_215_263_458 + index * 100_000_000), + true, + ); + } const result = await fetchVegetationShadowResult(resultId, { - fetcher: async () => new Response(JSON.stringify(payload), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), + fetcher: async (url) => String(url).endsWith("/route-timeline") + ? new Response(timeline, { + status: 200, + headers: { + "Content-Type": "application/octet-stream", + ETag: `"${"5".repeat(64)}"`, + }, + }) + : new Response(JSON.stringify(payload), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), }); assert.equal(result.routeVideo, null); assert.equal(result.routeFullReview.frameCount, 6830); assert.equal(result.routeFullReview.city.taxonomy.length, 16); assert.equal(result.routeFullReview.vegetation.taxonomy.length, 64); assert.equal(result.routeFullReview.decodeRepair.sequence, 6092); + assert.equal(result.routeFullReview.frameSourceTimesNs.length, 6830); assert.equal( vegetationFullRouteMaskUrl(resultId, "vegetation", 6829), `/api/v1/laboratory/vegetation-shadow/${resultId}/route-masks/vegetation/6829`, diff --git a/src/k1link/laboratory/mixed_route_vegetation_review.py b/src/k1link/laboratory/mixed_route_vegetation_review.py index fc67c36..48675f8 100644 --- a/src/k1link/laboratory/mixed_route_vegetation_review.py +++ b/src/k1link/laboratory/mixed_route_vegetation_review.py @@ -7,6 +7,7 @@ import hashlib import json import os import shutil +import struct import tarfile import tempfile import zipfile @@ -705,6 +706,18 @@ def seal_mixed_route_full_video_review( temporary / "video" / "ddrnet-semantic-masks.zip", FULL_ROUTE_FRAME_COUNT, ) + timeline_destination = temporary / "video" / "frame-source-times-ns.bin" + timeline_destination.write_bytes( + struct.pack(f"<{FULL_ROUTE_FRAME_COUNT}Q", *frame_times_ns) + ) + timeline_descriptor = { + "role": "full-route-frame-timeline", + "path": "video/frame-source-times-ns.bin", + "byte_length": timeline_destination.stat().st_size, + "sha256": sha256_path(timeline_destination), + "media_type": "application/octet-stream", + } + artifacts.append(timeline_descriptor) proof_descriptors: dict[str, dict[str, object]] = {} for key, path in ( ("base", base_root / "result.json"), @@ -739,7 +752,13 @@ def seal_mixed_route_full_video_review( "height": 600, "timeline_start_seconds": job.timeline_start_seconds, "timeline_end_seconds": job.timeline_end_seconds, - "frame_source_times_ns": frame_times_ns, + "timeline": { + "path": timeline_descriptor["path"], + "sha256": timeline_descriptor["sha256"], + "byte_length": timeline_descriptor["byte_length"], + "encoding": "uint64-le-nanoseconds", + "frame_count": FULL_ROUTE_FRAME_COUNT, + }, "ground_truth": False, "decode_repair": { "repaired_frame_count": 1, diff --git a/src/k1link/web/vegetation_shadow_lab_api.py b/src/k1link/web/vegetation_shadow_lab_api.py index dbb3be7..1ae9bde 100644 --- a/src/k1link/web/vegetation_shadow_lab_api.py +++ b/src/k1link/web/vegetation_shadow_lab_api.py @@ -214,6 +214,54 @@ def _build_vegetation_lab_router( raise HTTPException(status_code=404, detail="Full-route semantic mask not found") return _zip_mask_response(candidate.joinpath(*relative.parts), sequence) + @router.get("/{result_id}/route-timeline") + def get_full_route_timeline(result_id: str) -> FileResponse: + candidate = _resolve_candidate(root_provider, definition, result_id) + manifest = _read_verified(candidate, definition) + route = manifest.get("route_full_review") + timeline = route.get("timeline") if isinstance(route, dict) else None + relative_text = timeline.get("path") if isinstance(timeline, dict) else None + frame_count = timeline.get("frame_count") if isinstance(timeline, dict) else None + byte_length = timeline.get("byte_length") if isinstance(timeline, dict) else None + sha256 = timeline.get("sha256") if isinstance(timeline, dict) else None + if ( + not isinstance(relative_text, str) + or frame_count != route.get("frame_count") + or byte_length != frame_count * 8 + or not isinstance(sha256, str) + or len(sha256) != 64 + ): + raise HTTPException(status_code=404, detail="Full-route timeline not found") + relative = PurePosixPath(relative_text) + artifacts = manifest.get("artifacts") + if ( + relative.is_absolute() + or str(relative) != relative_text + or any(part in {"", ".", ".."} for part in relative.parts) + or not isinstance(artifacts, list) + or not any( + isinstance(item, dict) + and item.get("path") == relative_text + and item.get("byte_length") == byte_length + and item.get("sha256") == sha256 + and item.get("media_type") == "application/octet-stream" + for item in artifacts + ) + ): + raise HTTPException(status_code=404, detail="Full-route timeline not found") + path = candidate.joinpath(*relative.parts) + if not path.is_file() or path.is_symlink() or path.stat().st_size != byte_length: + raise HTTPException(status_code=404, detail="Full-route timeline not found") + return FileResponse( + path, + media_type="application/octet-stream", + headers={ + "Cache-Control": "private, max-age=31536000, immutable", + "ETag": f'"{sha256}"', + "X-Content-Type-Options": "nosniff", + }, + ) + return router diff --git a/tests/test_vegetation_shadow_lab.py b/tests/test_vegetation_shadow_lab.py index f986067..703064f 100644 --- a/tests/test_vegetation_shadow_lab.py +++ b/tests/test_vegetation_shadow_lab.py @@ -4,6 +4,7 @@ import hashlib import io import json import shutil +import struct import zipfile from pathlib import Path from types import SimpleNamespace @@ -271,9 +272,17 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence( assert mask.headers["cache-control"].endswith("immutable") full_archive_payloads = (b"\x89PNG\r\n\x1a\ncity", b"\x89PNG\r\n\x1a\nvegetation") + full_timeline_payload = struct.pack("<2Q", 1_000_000_000, 1_100_000_000) full_identity = dict(manifest["identity"]) full_route = { "frame_count": 2, + "timeline": { + "path": "video/frame-source-times-ns.bin", + "sha256": hashlib.sha256(full_timeline_payload).hexdigest(), + "byte_length": len(full_timeline_payload), + "encoding": "uint64-le-nanoseconds", + "frame_count": 2, + }, "layers": { layer: {"mask_archive": {"path": "video/full-route-masks.zip"}} for layer in ("city", "vegetation") @@ -296,6 +305,8 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence( with zipfile.ZipFile(full_archive, "x", compression=zipfile.ZIP_STORED) as frozen: for sequence, payload in enumerate(full_archive_payloads, start=1): frozen.writestr(f"masks/frame-{sequence:06d}.png", payload) + full_timeline = full_root / "video" / "frame-source-times-ns.bin" + full_timeline.write_bytes(full_timeline_payload) full_manifest = dict(manifest) full_manifest["result_id"] = full_result_id full_manifest["identity"] = full_identity @@ -310,6 +321,13 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence( "sha256": _sha256(full_archive), "media_type": "application/zip", }, + { + "role": "full-route-frame-timeline", + "path": "video/frame-source-times-ns.bin", + "byte_length": full_timeline.stat().st_size, + "sha256": _sha256(full_timeline), + "media_type": "application/octet-stream", + }, ] (full_root / "result.json").write_text( json.dumps(full_manifest, ensure_ascii=False, sort_keys=True, separators=(",", ":")) @@ -330,6 +348,12 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence( assert client.get( f"/api/v1/laboratory/vegetation-shadow/{full_result_id}/route-masks/city/2" ).status_code == 404 + timeline = client.get( + f"/api/v1/laboratory/vegetation-shadow/{full_result_id}/route-timeline" + ) + assert timeline.status_code == 200 + assert timeline.content == full_timeline_payload + assert timeline.headers["cache-control"].endswith("immutable") (result_root / asset_path).write_bytes(b"tampered") assert ( From 88e47fc39a80d587e82440de645571fa18ba6954 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 29 Aug 2026 14:14:38 +0300 Subject: [PATCH 5/6] fix(lab): overlay full route semantic masks --- .../laboratory/VegetationShadowResult.tsx | 22 ++++++++++--------- .../test/vegetationShadow.test.mjs | 1 + 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx b/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx index 642e3e2..7b7840b 100644 --- a/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx +++ b/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx @@ -159,16 +159,18 @@ function FullRouteReviewEvidence({ {mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`}
{layer && semantic ? ( - +
+ +
) : null} )} diff --git a/apps/control-station/test/vegetationShadow.test.mjs b/apps/control-station/test/vegetationShadow.test.mjs index d4c96ba..133ecd8 100644 --- a/apps/control-station/test/vegetationShadow.test.mjs +++ b/apps/control-station/test/vegetationShadow.test.mjs @@ -384,6 +384,7 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr assert.match(resultSource, /RAVNOVES004TREE mixed route review/); assert.match(resultSource, /RAVNOVES004TREE full recorded review/); assert.match(resultSource, /LaboratoryRecordedClipPlayer/); + assert.match(resultSource, /className="m48-clip-player__overlay"/); assert.match(resultSource, /linkedTgsResultId/); assert.match(benchmarkSource, /M48MaskComparisonVisual/); assert.doesNotMatch(benchmarkSource, /M49TgsFullShadowEvidence/); From 1c7dd29d8a9209e771d37b4e8633eab7bca482d1 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 21 Sep 2026 08:45:34 +0300 Subject: [PATCH 6/6] chore(node): preserve pre-canonicalization experiment snapshot Historical working copy retained for audit before consolidation into main. The canonicalized plugin architecture and later fixes already live in main; this snapshot is not a release or a request to restore obsolete source layout. --- .../src/components/RerunViewport.tsx | 12 +- .../laboratory/CanonicalResultRerunReplay.tsx | 5 +- .../core/environment/environmentSettings.ts | 6 +- .../src/core/observation/viewerProfile.ts | 21 +- apps/control-station/src/productModel.ts | 12 +- .../src/workspaces/fleet/VehicleSensors.tsx | 8 +- .../test/applicationArchitecture.test.mjs | 2 +- .../test/devicePluginContracts.test.mjs | 18 +- .../devicePluginFrontendBoundary.test.mjs | 22 +- .../test/environmentSettings.test.mjs | 2 +- .../test/k1SupervisorPresentation.test.mjs | 78 +++- .../test/networkProvisioning.test.mjs | 132 +++++++ .../test/observatoryWorkspace.test.mjs | 4 +- .../test/polygonRunArchive.test.mjs | 2 +- .../test/productShellContract.test.mjs | 2 +- .../test/viewerProfile.test.mjs | 13 + apps/node-agent/cmd/node-agent/main.go | 6 + .../internal/node/device_enrollment.go | 323 +++++++++++++++++ .../internal/node/device_enrollment_test.go | 111 ++++++ apps/node-agent/internal/node/pairing.go | 29 +- .../internal/node/pairing_transport.go | 16 + apps/node-agent/internal/node/sensors.go | 61 +++- apps/node-agent/internal/node/server.go | 32 +- .../packaging/50-mission-core-k1.rules | 6 + apps/node-agent/packaging/build.py | 4 +- apps/node-agent/packaging/build_deb.py | 27 +- .../packaging/fetch_driver_bundle.py | 33 +- .../packaging/install-k1-credential | 36 ++ apps/node-agent/packaging/k1-bundle.json | 265 ++++++++++++++ apps/node-agent/packaging/k1_bootstrap.py | 26 ++ apps/node-agent/packaging/k1_prepare.py | 94 +++++ .../packaging/mission-core-k1.service | 38 ++ apps/node-agent/packaging/postinst | 6 + apps/node-agent/packaging/preinst | 11 + apps/node-agent/packaging/prerm | 8 + .../node-agent/packaging/resolve_k1_bundle.py | 60 ++++ apps/node-agent/ui/package-lock.json | 20 +- apps/node-agent/ui/package.json | 2 + apps/node-agent/ui/rerun-runtime.html | 16 + apps/node-agent/ui/src/NodeSensors.tsx | 5 +- apps/node-agent/ui/src/rerunRuntime.ts | 1 + apps/node-agent/ui/vite.config.js | 8 + docs/04_K1_WIFI_PROVISIONING_PROFILE.md | 19 + ...026-09-06-k1-bridge-connection-incident.md | 140 ++++++++ ...026-09-06-k1-live-reference-camera-root.md | 129 +++++++ .../2026-09-06-k1-station-reply-semantics.md | 95 +++++ ...026-09-06-node-k1-bridge-implementation.md | 164 +++++++++ .../sensor-ui/src/DeviceEnrollmentWindow.tsx | 49 +++ packages/sensor-ui/src/K1Detail.tsx | 25 ++ packages/sensor-ui/src/K1LiveSettings.tsx | 28 ++ packages/sensor-ui/src/K1LiveView.tsx | 63 ++++ packages/sensor-ui/src/SensorWorkspace.tsx | 19 +- packages/sensor-ui/src/contracts.ts | 5 +- packages/sensor-ui/src/enrollment.ts | 37 ++ packages/sensor-ui/src/rerunHost.ts | 12 + packages/sensor-ui/src/sensors.css | 4 + .../frontend/src/XgridsK1Connection.tsx | 11 + .../src/components/K1OperatorError.tsx | 6 +- .../src/components/K1ProvisioningPipeline.tsx | 21 +- .../xgrids-k1/frontend/src/configuration.ts | 3 + .../src/networkFailurePresentation.ts | 7 + .../frontend/src/networkProvisioning.ts | 241 +++++++++++++ .../xgrids-k1/frontend/src/presentation.ts | 6 +- .../frontend/src/useXgridsK1Runtime.ts | 337 ++---------------- pyproject.toml | 1 + .../device_plugins/xgrids_k1/ble/scanner.py | 109 +++++- src/k1link/device_plugins/xgrids_k1/camera.py | 15 +- src/k1link/device_plugins/xgrids_k1/facade.py | 54 ++- .../device_plugins/xgrids_k1/linux_host.py | 214 +++++++++++ .../device_plugins/xgrids_k1/node_bridge.py | 248 +++++++++++++ .../device_plugins/xgrids_k1/node_sensor.py | 255 +++++++++++++ .../device_plugins/xgrids_k1/wifi_failure.py | 33 ++ src/k1link/fleet/device_enrollment.py | 173 +++++++++ src/k1link/fleet/registry.py | 6 +- src/k1link/viewer/node_media.py | 170 +++++++++ src/k1link/viewer/node_rerun.py | 144 ++++++++ src/k1link/viewer/rerun_bridge.py | 47 ++- src/k1link/web/fleet_api.py | 49 +++ src/k1link/web/runtime_diagnostics.py | 35 ++ tests/fleet/test_device_enrollment.py | 111 ++++++ tests/test_ble_scanner.py | 127 ++++++- tests/test_k1_wifi_failure.py | 72 ++++ tests/test_node_k1_bridge.py | 204 +++++++++++ tests/test_node_media.py | 102 ++++++ tests/test_viewer_diagnostics_api.py | 49 +++ tests/test_xgrids_acquisition_lifecycle.py | 66 +++- tests/test_xgrids_ble_runtime_arbiter.py | 2 +- tests/test_xgrids_camera_gateway.py | 93 +++++ uv.lock | 130 ++++++- 89 files changed, 5026 insertions(+), 487 deletions(-) create mode 100644 apps/control-station/test/networkProvisioning.test.mjs create mode 100644 apps/node-agent/internal/node/device_enrollment.go create mode 100644 apps/node-agent/internal/node/device_enrollment_test.go create mode 100644 apps/node-agent/packaging/50-mission-core-k1.rules create mode 100644 apps/node-agent/packaging/install-k1-credential create mode 100644 apps/node-agent/packaging/k1-bundle.json create mode 100644 apps/node-agent/packaging/k1_bootstrap.py create mode 100644 apps/node-agent/packaging/k1_prepare.py create mode 100644 apps/node-agent/packaging/mission-core-k1.service create mode 100644 apps/node-agent/packaging/resolve_k1_bundle.py create mode 100644 apps/node-agent/ui/rerun-runtime.html create mode 100644 apps/node-agent/ui/src/rerunRuntime.ts create mode 100644 docs/audits/2026-09-06-k1-bridge-connection-incident.md create mode 100644 docs/audits/2026-09-06-k1-live-reference-camera-root.md create mode 100644 docs/audits/2026-09-06-k1-station-reply-semantics.md create mode 100644 docs/audits/2026-09-06-node-k1-bridge-implementation.md create mode 100644 packages/sensor-ui/src/DeviceEnrollmentWindow.tsx create mode 100644 packages/sensor-ui/src/K1Detail.tsx create mode 100644 packages/sensor-ui/src/K1LiveSettings.tsx create mode 100644 packages/sensor-ui/src/K1LiveView.tsx create mode 100644 packages/sensor-ui/src/enrollment.ts create mode 100644 packages/sensor-ui/src/rerunHost.ts create mode 100644 plugins/xgrids-k1/frontend/src/networkFailurePresentation.ts create mode 100644 plugins/xgrids-k1/frontend/src/networkProvisioning.ts create mode 100644 src/k1link/device_plugins/xgrids_k1/linux_host.py create mode 100644 src/k1link/device_plugins/xgrids_k1/node_bridge.py create mode 100644 src/k1link/device_plugins/xgrids_k1/node_sensor.py create mode 100644 src/k1link/device_plugins/xgrids_k1/wifi_failure.py create mode 100644 src/k1link/fleet/device_enrollment.py create mode 100644 src/k1link/viewer/node_media.py create mode 100644 src/k1link/viewer/node_rerun.py create mode 100644 tests/fleet/test_device_enrollment.py create mode 100644 tests/test_k1_wifi_failure.py create mode 100644 tests/test_node_k1_bridge.py create mode 100644 tests/test_node_media.py diff --git a/apps/control-station/src/components/RerunViewport.tsx b/apps/control-station/src/components/RerunViewport.tsx index 4857bd8..5038db7 100644 --- a/apps/control-station/src/components/RerunViewport.tsx +++ b/apps/control-station/src/components/RerunViewport.tsx @@ -762,7 +762,15 @@ export async function probeRecordedPerceptionViewerSource( return { sourceUrl: endpoint.href, byteLength: declaredLength }; } -export function RerunViewport({ +export function RerunViewport(props: RerunViewportProps) { + // Even when source URLs coincide, another profile cannot inherit a mounted + // viewer's playback, blueprint channels, recovery state or scene draft. + const identity = props.profile.kind === "laboratory-result" + ? `${props.profile.kind}:${props.profile.resultId}` : props.profile.kind; + return ; +} + +function RerunViewportInstance({ profile, onStatusChange, onSelectionChange, @@ -772,7 +780,7 @@ export function RerunViewport({ onPerceptionLoadChange, onPointColorLoadChange, }: RerunViewportProps) { - const recordedProfile = profile.kind === "recorded-session" ? profile : null; + const recordedProfile = profile.kind === "recorded-session" || profile.kind === "laboratory-result" ? profile : null; const liveProfile = profile.kind === "live-acquisition" ? profile : null; const sourceUrl = profile.sourceUrl; const recordedArtifact = recordedProfile?.artifact ?? null; diff --git a/apps/control-station/src/components/laboratory/CanonicalResultRerunReplay.tsx b/apps/control-station/src/components/laboratory/CanonicalResultRerunReplay.tsx index 9ee4126..63d102b 100644 --- a/apps/control-station/src/components/laboratory/CanonicalResultRerunReplay.tsx +++ b/apps/control-station/src/components/laboratory/CanonicalResultRerunReplay.tsx @@ -34,7 +34,7 @@ import { } from "../laboratory/CanonicalRecordedLabReplay"; import type { CanonicalLabReplayDescriptor } from "../../core/laboratory/canonicalLabReplay"; import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive"; -import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile"; +import { laboratoryResultRerunProfile } from "../../core/observation/viewerProfile"; import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions"; import type { AIViewerLayer } from "../../core/observatory/aiComposition"; import { @@ -305,7 +305,8 @@ export function CanonicalResultRerunReplay({ accumulationSeconds: showLocalSlam ? sceneDraft.accumulationSeconds : 0, }), [sceneDraft, showLocalSlam, showSourcePoints, spatialMode]); const semanticVisible = showDDRNet || showEoMT; - const profile = launch ? recordedSessionRerunProfile({ + const profile = launch ? laboratoryResultRerunProfile({ + resultId, sourceUrl: launch.replay.sourceUrl, artifact: { sourceUrl: launch.replay.sourceUrl, diff --git a/apps/control-station/src/core/environment/environmentSettings.ts b/apps/control-station/src/core/environment/environmentSettings.ts index 2c2cd3d..80250f1 100644 --- a/apps/control-station/src/core/environment/environmentSettings.ts +++ b/apps/control-station/src/core/environment/environmentSettings.ts @@ -113,13 +113,13 @@ const defaultQuickActions: Record< EnvironmentSurfaceId, readonly [string | null, string | null] > = { - home: ["spatial-scene", "local-device"], - fleet: ["contour-health", "local-device"], + home: ["spatial-scene", "vehicles"], + fleet: ["vehicles", "contour-health"], observation: ["spatial-scene", "cameras"], missions: ["mission-planner", null], data: ["recordings", "datasets"], system: ["modules", "integrations"], - polygon: ["lab-archive", null], + polygon: ["lab-archive", "local-device"], }; export function defaultEnvironmentSettings(): EnvironmentSettings { diff --git a/apps/control-station/src/core/observation/viewerProfile.ts b/apps/control-station/src/core/observation/viewerProfile.ts index 1abd1e7..91d5210 100644 --- a/apps/control-station/src/core/observation/viewerProfile.ts +++ b/apps/control-station/src/core/observation/viewerProfile.ts @@ -85,6 +85,13 @@ export interface RecordedSessionRerunProfile { lockPerceptionCameraInteraction: boolean; } +/** A LAB owns its result presentation; it does not inherit session-view settings. */ +export interface LaboratoryResultRerunProfile + extends Omit { + kind: "laboratory-result"; + resultId: string; +} + /** * Deprecated comparison-only contract for LAB artifacts that have not yet * been republished as a native Rerun sidecar. It must never be selected by a @@ -101,7 +108,8 @@ export interface LaboratoryRecordedEvidenceViewerProfile { export type RerunViewerProfile = | LiveAcquisitionRerunProfile - | RecordedSessionRerunProfile; + | RecordedSessionRerunProfile + | LaboratoryResultRerunProfile; export type ObservationViewerProfile = | RerunViewerProfile @@ -119,11 +127,18 @@ export const LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE = Object.freeze({ export function liveAcquisitionRerunProfile( input: Omit, ): LiveAcquisitionRerunProfile { - return { kind: "live-acquisition", clock: "stream_time", ...input }; + return { ...input, kind: "live-acquisition", clock: "stream_time" }; } export function recordedSessionRerunProfile( input: Omit, ): RecordedSessionRerunProfile { - return { kind: "recorded-session", clock: "session_time", ...input }; + return { ...input, kind: "recorded-session", clock: "session_time" }; +} + +export function laboratoryResultRerunProfile( + input: Omit, +): LaboratoryResultRerunProfile { + if (!input.resultId.trim()) throw new Error("LAB result identity is required"); + return { ...input, kind: "laboratory-result", clock: "session_time" }; } diff --git a/apps/control-station/src/productModel.ts b/apps/control-station/src/productModel.ts index 5e6872c..ed57b5d 100644 --- a/apps/control-station/src/productModel.ts +++ b/apps/control-station/src/productModel.ts @@ -94,7 +94,7 @@ export const roots: RootDefinition[] = [ label: "Парк", title: "Аппараты и устройства", eyebrow: "ПАРК / УСТРОЙСТВА", - description: "Реестр аппаратов, локальное подключение, сенсоры и конфигурации борта.", + description: "Реестр аппаратов, бортовые компьютеры, сенсоры и конфигурации борта.", statement: "Одинаково подключать одиночный стенд, наземную платформу и будущий рой.", accent: "АППАРАТЫ И ПОЛЕЗНАЯ НАГРУЗКА", }, @@ -203,11 +203,11 @@ export const workspaces: WorkspaceDefinition[] = [ }, { id: "local-device", - root: "fleet", - label: "Подключение", - title: "Подключение", - eyebrow: "ПАРК / ТЕКУЩИЙ АДАПТЕР", - description: "Выбор модели, сценарий установленного плагина и запуск доступного потока.", + root: "polygon", + label: "Тестовые устройства", + title: "Тестовые устройства", + eyebrow: "LAB / ТЕСТОВЫЕ УСТРОЙСТВА", + description: "Подключение устройств к компьютеру оператора для испытаний и записи.", icon: "network", kind: "device", groups: [], diff --git a/apps/control-station/src/workspaces/fleet/VehicleSensors.tsx b/apps/control-station/src/workspaces/fleet/VehicleSensors.tsx index 6bcbdd7..b92c41c 100644 --- a/apps/control-station/src/workspaces/fleet/VehicleSensors.tsx +++ b/apps/control-station/src/workspaces/fleet/VehicleSensors.tsx @@ -1,13 +1,19 @@ import {useMemo} from 'react'; +import {createIsolatedRerunHost} from '../../components/rerun/isolatedRerunHost'; import {SensorWorkspace} from '../../../../../packages/sensor-ui/src/SensorWorkspace'; import type {SensorInventory,SensorTransport} from '../../../../../packages/sensor-ui/src/contracts'; import {fleetRequest} from '../../core/fleet/useFleet'; export function VehicleSensors({vehicleID,enabled,onDetailChange}:{vehicleID:string;enabled:boolean;onDetailChange:(open:boolean)=>void}){ const transport=useMemo(()=>({ + enrollment:{ + state:()=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment`), + submit:value=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment/operations`,'POST',value), + operation:id=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment/operations/${encodeURIComponent(id)}`), + }, inventory:async()=>{const fleet=await fleetRequest<{items:{id:string;connectivity:string;sensor_state:SensorInventory}[]}>();const value=fleet.items.find(v=>v.id===vehicleID);if(!value)throw new Error('Аппарат не найден.');return {...value.sensor_state,fresh:value.connectivity==='online'};}, subscribe:(receive,unavailable)=>{const events=new EventSource('/api/v1/fleet/events');events.onmessage=e=>{try{const value=JSON.parse(e.data).items.find((v:{id:string})=>v.id===vehicleID);if(value)receive({...value.sensor_state,fresh:value.connectivity==='online'});else unavailable();}catch{unavailable();}};events.onerror=unavailable;return()=>events.close();}, submit:value=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/operations`,'POST',value), operation:id=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/operations/${encodeURIComponent(id)}`), }),[vehicleID]); - return ; + return ; } diff --git a/apps/control-station/test/applicationArchitecture.test.mjs b/apps/control-station/test/applicationArchitecture.test.mjs index 359ef60..d17addd 100644 --- a/apps/control-station/test/applicationArchitecture.test.mjs +++ b/apps/control-station/test/applicationArchitecture.test.mjs @@ -143,7 +143,7 @@ test("Observatory owns a bounded explicit-open lifecycle around the shared recor /advanced-index|resolveObservationSessionReplay|resolveCanonicalLabReplay|RerunViewport/, ); assert.equal(sharedReplay.match(/\(hasTgs \? 1 : 0\)/); assert.match(sharedReplay, /hasTgs \? 0\.000001 : 0/); for (const adapter of ["CanonicalVegetationRerunReplay", "PortableResultReplay"]) { diff --git a/apps/control-station/test/devicePluginContracts.test.mjs b/apps/control-station/test/devicePluginContracts.test.mjs index 136c824..5dde64b 100644 --- a/apps/control-station/test/devicePluginContracts.test.mjs +++ b/apps/control-station/test/devicePluginContracts.test.mjs @@ -1676,7 +1676,7 @@ test("physical-command guidance projects backend policy without exposing reason ); }); -test("restart-recovery guidance describes automatic recovery without protocol ceremony", () => { +test("restart-recovery guidance offers an explicit read-only recovery", () => { const deniedFresh = { connection_policy: { schema_version: "missioncore.xgrids-k1-connection-policy/v1", @@ -1712,7 +1712,7 @@ test("restart-recovery guidance describes automatic recovery without protocol ce ); assert.deepEqual(durableGuidance, { reason: "В свежем Bluetooth-поиске не найден K1, связанный с незавершённой записью.", - nextAction: "Дождитесь автоматического восстановления сохранённого подключения K1.", + nextAction: "Нажмите «Переподключиться», чтобы проверить сохранённое подключение K1.", }); assert.doesNotMatch( `${durableGuidance.reason} ${durableGuidance.nextAction}`, @@ -1725,7 +1725,7 @@ test("restart-recovery guidance describes automatic recovery without protocol ce deniedFresh, "observe-fresh-device-network", ).nextAction, - "Дождитесь автоматического восстановления связи с тем же K1.", + "Нажмите «Переподключиться», чтобы проверить связь с тем же K1.", ); }); @@ -3461,7 +3461,7 @@ test("each terminal explicit provisioning click starts a fresh operation identit assert.match( connectRecovery, - /failedOperation\?\.status === "succeeded"[\s\S]*?!exactAppliedNetworkIntentCompleted\([\s\S]*?&& !hasExactConnectionReady/, + /observeProvisioningRequest\([\s\S]*?observedState = observation\.state[\s\S]*?const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\(/, ); assert.match( connectRecovery, @@ -4009,7 +4009,7 @@ test("failed network-profile writes close the UI session with a fresh explicit n assert.equal( message, - "Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён. Автоматический повтор команды K1 не отправлялся. Сессия подключения в интерфейсе сброшена. Выполните новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз.", + "Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.", ); assert.doesNotMatch(message, /Bleak|GATT|ATT/i); @@ -4024,8 +4024,8 @@ test("failed network-profile writes close the UI session with a fresh explicit n }); assert.match(attMessage, /ATT 4 INVALID_PDU/); assert.match(attMessage, /Автоматический повтор команды K1 не отправлялся/); - assert.match(attMessage, /Сессия подключения в интерфейсе сброшена/); - assert.match(attMessage, /новый поиск Bluetooth, выберите K1/); + assert.match(attMessage, /Проверьте состояние K1 через «Переподключиться»/); + assert.match(attMessage, /новое подключение через поиск Bluetooth/); const preWriteAttMessage = networkProvisionFailureMessage({ action: "network.provision", @@ -4059,7 +4059,7 @@ test("post-dispatch ambiguity resets the UI session without an automatic retry", assert.equal( message, - "После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Сессия подключения в интерфейсе сброшена. Выполните новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз.", + "После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.", ); assert.doesNotMatch(message, /ручн|read-only|защитный барьер/i); }); @@ -4247,7 +4247,7 @@ test("host Wi-Fi failures close the attempt and require a fresh explicit connect assert.equal( operationTimeout, - "Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Сессия подключения в интерфейсе сброшена. Выполните новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз.", + "Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.", ); assert.doesNotMatch(operationTimeout, /получите пароль|пароль отсутствует/i); assert.match(missingNetwork, /K1 принял команду Quick Connect/); diff --git a/apps/control-station/test/devicePluginFrontendBoundary.test.mjs b/apps/control-station/test/devicePluginFrontendBoundary.test.mjs index 6bdb1ee..08d2add 100644 --- a/apps/control-station/test/devicePluginFrontendBoundary.test.mjs +++ b/apps/control-station/test/devicePluginFrontendBoundary.test.mjs @@ -218,8 +218,8 @@ test("K1 connection surface enforces one scan, local selection, and one Apply", ); assert.match(provisioning, /scanSecondsRemaining/); assert.match(provisioning, /setInterval\(updateCountdown, 250\)/); - assert.match(provisioning, /Поиск Bluetooth · \{scanSecondsRemaining \?\? 6\} с/); - assert.match(provisioning, /scanWithResult\(\{[^}]*durationSeconds:\s*6/); + assert.match(provisioning, /Поиск Bluetooth · до \{scanSecondsRemaining \?\? BLE_DISCOVERY_TIMEOUT_SECONDS\} с/); + assert.match(provisioning, /scanWithResult\(\{[^}]*durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/); assert.doesNotMatch(provisioning, /K1 уже доступен|Сетевой адрес K1 доступен/); assert.doesNotMatch( provisioning, @@ -284,7 +284,7 @@ test("K1 click-owned actions are fenced without hidden frontend continuations", provisioning.indexOf("const verifyAppliedNetwork"), ); assert.equal((search.match(/scanWithResult\(/g) ?? []).length, 1); - assert.match(search, /durationSeconds:\s*6/); + assert.match(search, /durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/); assert.equal((apply.match(/await connect\(/g) ?? []).length, 1); assert.doesNotMatch( apply, @@ -680,7 +680,7 @@ test("automatic K1 live start opens the selected delivered camera despite an old assert.match(layout, /source\.capabilities\.overlay/); }); -test("K1 connect errors reset the UI session without exposing reconciliation ceremony", () => { +test("K1 connect errors preserve the result and expose explicit recovery", () => { const runtime = readFileSync( join(pluginFrontendRoot, "useXgridsK1Runtime.ts"), "utf8", @@ -691,15 +691,17 @@ test("K1 connect errors reset the UI session without exposing reconciliation cer assert.notEqual(connectEnd, -1); const connectFlow = runtime.slice(connectStart, connectEnd); - assert.match(connectFlow, /operationByIdempotencyKey\(/); - assert.match(connectFlow, /failedOperation\?\.status === "succeeded"/); - assert.match(connectFlow, /resetConnectSessionMessage\(/); + assert.match(connectFlow, /observeProvisioningRequest\(/); + assert.match(connectFlow, /operation\?\.status !== "succeeded"/); + assert.match(connectFlow, /networkProvisionFailureMessage\(/); assert.doesNotMatch( connectFlow, /требует ручной проверки|измените параметры только после проверки устройства|Сохранён тот же ключ/, ); - assert.match(runtime, /Сессия подключения в интерфейсе сброшена/); - assert.match(runtime, /новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз/); + const networkFlow = readFileSync(join(pluginFrontendRoot, "networkProvisioning.ts"), "utf8"); + assert.match(networkFlow, /Проверьте состояние K1 через «Переподключиться»/); + assert.match(networkFlow, /новое подключение через поиск Bluetooth/); + assert.equal((networkFlow.match(/await context\.send\(/g) ?? []).length, 1); const verifyStart = runtime.indexOf("const verifyConnection = useCallback("); const verifyEnd = runtime.indexOf("const probeConfiguredEndpoint = useCallback(", verifyStart); const verifyFlow = runtime.slice(verifyStart, verifyEnd); @@ -729,7 +731,7 @@ test("K1 provisioning keeps the operator draft separate from the backend lease", assert.match(selection, /requestExplicitProvisioning\(device\.device_id/); assert.equal((apply.match(/await connect\(/g) ?? []).length, 1); assert.doesNotMatch(apply, /scanWithResult\(|verifyConnection\(|candidateRefresh/); - assert.match(provisioning, /scanWithResult\(\{ durationSeconds: 6 \}\)/); + assert.match(provisioning, /scanWithResult\(\{ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS \}\)/); assert.match( provisioning, /onChange=\{\(event\) => setPassword\(event\.target\.value\)\}/, diff --git a/apps/control-station/test/environmentSettings.test.mjs b/apps/control-station/test/environmentSettings.test.mjs index 144c3e5..5f7b3ca 100644 --- a/apps/control-station/test/environmentSettings.test.mjs +++ b/apps/control-station/test/environmentSettings.test.mjs @@ -65,7 +65,7 @@ test("default environment exposes every current product page", () => { "system", "polygon", ]); - assert.equal(defaults.pages.fleet.primaryWorkspaceId, "contour-health"); + assert.equal(defaults.pages.fleet.primaryWorkspaceId, "vehicles"); assert.equal(defaults.pages.observation.primaryWorkspaceId, "spatial-scene"); }); diff --git a/apps/control-station/test/k1SupervisorPresentation.test.mjs b/apps/control-station/test/k1SupervisorPresentation.test.mjs index 4c17290..0d42914 100644 --- a/apps/control-station/test/k1SupervisorPresentation.test.mjs +++ b/apps/control-station/test/k1SupervisorPresentation.test.mjs @@ -250,7 +250,7 @@ test("K1 one-intent source contract makes mode reset explicit and keeps device I "const chooseAnother", ); - assert.match(search, /scanWithResult\(\{[^}]*durationSeconds:\s*6/); + assert.match(search, /scanWithResult\(\{[^}]*durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/); assert.equal((search.match(/scanWithResult\(/g) ?? []).length, 1); assert.doesNotMatch(search, /\b(?:connect|verifyConnection|submitConnect)\s*\(/); @@ -295,7 +295,7 @@ test("K1 one-intent source contract makes mode reset explicit and keeps device I } assert.equal((source.match(/buttonLabel:\s*"Применить"/g) ?? []).length, 3); assert.match(source, /устарел|stale/i); - assert.match(source, /неизвест|outcome-unknown|safe_to_retry/i); + assert.match(source, /network_outcome_unknown|safe_to_retry/i); }); test("exact network-applied REST proof completes frontend Apply before control is ready", () => { @@ -377,7 +377,7 @@ test("exact network-applied REST proof completes frontend Apply before control i ); assert.match( connectFlow, - /failedOperation\?\.status === "succeeded"[\s\S]*?!exactAppliedNetworkIntentCompleted\([\s\S]*?&& !hasExactConnectionReady/, + /observeProvisioningRequest\([\s\S]*?observedState = observation\.state[\s\S]*?const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\(/, ); assert.doesNotMatch( connectFlow, @@ -1359,7 +1359,7 @@ function actionByLabel(node, label) { return actionByLabel(node.props.children, label); } -function renderProvisioningWithAttempt(props, presentation) { +function renderProvisioningWithAttempt(props, presentation, afterSearch = false) { function AttemptHarness() { const internals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; @@ -1367,6 +1367,12 @@ function renderProvisioningWithAttempt(props, presentation) { assert.equal(typeof dispatcher?.useState, "function"); const originalUseState = dispatcher.useState; dispatcher.useState = (initialState) => { + if (afterSearch && initialState === emptySearchPresentation) { + const state = props.controller.state; + return [{ sequence: 1, snapshotRuntimeId: state.snapshot_runtime_id, + connectionMode: props.desiredMode, desiredModeRevision: state.desired_connection_mode_revision, + active: false, completedDiscoveryGeneration: state.ble_discovery_generation }, () => undefined]; + } if (initialState === emptyProvisioningAttemptPresentation) { return [presentation, () => undefined]; } @@ -2487,10 +2493,11 @@ test("toolbar reset in one runtime rejects late Scan A and admits exactly one Sc } }); -test("local connection navigation and selector copy use process nouns", () => { +test("operator-local connection belongs to LAB and retains all test modes", () => { const localConnection = workspaces.find((workspace) => workspace.id === "local-device"); - assert.equal(localConnection?.label, "Подключение"); - assert.equal(localConnection?.title, "Подключение"); + assert.equal(localConnection?.root, "polygon"); + assert.equal(localConnection?.label, "Тестовые устройства"); + assert.equal(localConnection?.title, "Тестовые устройства"); assert.equal(model().displayName, "XGRIDS LixelKity K1"); assert.deepEqual( connectionModeOptions.map(({ label }) => label), @@ -7185,7 +7192,7 @@ test("a current-authority Bluetooth scan owns Step 01 and no network step", () = }); assert.match(markup, /01<\/span>/); - assert.match(markup, /Поиск Bluetooth · 6 с/); + assert.match(markup, /Поиск Bluetooth · до 20 с/); assert.equal( (markup.match(/class="nodedc-activity-indicator"/g) ?? []).length, 1, @@ -7478,7 +7485,7 @@ test("applied-network recovery spends the old intent and admits only explicit se search, /const recoveryScanAttempt = appliedRecoveryEscape[\s\S]*?canScanAppliedRecovery[\s\S]*?unresolvedAppliedAttempt/, ); - assert.match(search, /scanResult = await scanWithResult\(\{ durationSeconds: 6 \}\)/); + assert.match(search, /scanResult = await scanWithResult\(\{ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS \}\)/); assert.match( search, /if \(scanResult\.succeeded && recoveryAttemptKey\) \{\s*setEscapedAppliedAttemptKey\(recoveryAttemptKey\)/, @@ -7708,3 +7715,56 @@ test("contour health never promotes selection or replay metrics to live authorit }); assert.equal(replay.aiActive, false); }); + + +test("the recovery panel explains the reviewed K1 network-not-found reply", () => { + const state = terminalConnectionRecoveryState(); + state.connection_attempt.public_error_code = "k1-wifi-network-not-found"; + const markup = renderToStaticMarkup(createElement(K1OperatorError, { + attempt: state.connection_attempt, onRefresh() {}, onClear() {}, + })); + assert.match(markup, /K1 не смог найти указанную сеть Wi‑Fi/); + assert.match(markup, /точное название сети/); + assert.doesNotMatch(markup, /ошибкой Bluetooth|INVALID_PDU/); +}); + +test("a reviewed Wi-Fi rejection offers explicit network setup for the same K1", () => { + const state = terminalConnectionRecoveryState(); + state.connection_attempt.public_error_code = "k1-wifi-network-not-found"; + const controller = { + ...provisioningController(state), error: "private transport error", + errorCorrelation: { + action: "connect", runtimeId: state.snapshot_runtime_id, leaseGeneration: 0, + connectionAttemptId: state.connection_attempt.attempt_id, + }, + getConnectionRecoveryObservationTarget: () => recommendedConnectionRecoveryObservationTarget(state), + }; + const markup = renderProvisioningWithAttempt({ controller, desiredMode: "bridge" }, + provisioningAttemptPresentation({ snapshotRuntimeId: state.snapshot_runtime_id, + attemptId: state.connection_attempt.attempt_id, localPhase: "failed" }), true); + assert.equal(buttonMarkupWithText(markup, "Указать сеть Wi‑Fi заново").length, 1); + assert.doesNotMatch(markup, /Подключить новый K1|private transport error/); +}); + +test("a failed Connect after completed Bluetooth search exposes recovery instead of a locked form", () => { + const state = terminalConnectionRecoveryState(); + state.ble_discovery_generation = 6; + state.connection_attempt.public_error_code = "BleakGATTProtocolError"; + state.connection_attempt.stage = "gatt-write-failed"; + const controller = { + ...provisioningController(state), + error: "private transport exception must stay hidden", + errorCorrelation: { + action: "connect", runtimeId: state.snapshot_runtime_id, leaseGeneration: 0, + connectionAttemptId: state.connection_attempt.attempt_id, + }, + getConnectionRecoveryObservationTarget: () => recommendedConnectionRecoveryObservationTarget(state), + }; + const markup = renderProvisioningWithAttempt({ controller, desiredMode: "bridge" }, + provisioningAttemptPresentation({ snapshotRuntimeId: state.snapshot_runtime_id, + attemptId: state.connection_attempt.attempt_id, localPhase: "failed" }), true); + assert.equal(buttonMarkupWithText(markup, "Переподключиться").length, 1); + assert.equal(buttonMarkupWithText(markup, "Подключить новый K1").length, 1); + assert.match(markup, /K1 ответил ошибкой Bluetooth/); + assert.doesNotMatch(markup, /Подключение не выполнено|дождитесь, пока система|private transport exception|Пароль передан/); +}); diff --git a/apps/control-station/test/networkProvisioning.test.mjs b/apps/control-station/test/networkProvisioning.test.mjs new file mode 100644 index 0000000..2640c1b --- /dev/null +++ b/apps/control-station/test/networkProvisioning.test.mjs @@ -0,0 +1,132 @@ +import assert from "node:assert/strict"; +import { before, after, test } from "node:test"; +import { createServer } from "vite"; + +let server; +let observeProvisioningRequest; +let networkProvisionFailureMessage; +let ApiError; +before(async () => { + server = await createServer({ appType: "custom", logLevel: "silent", server: { middlewareMode: true } }); + ({ observeProvisioningRequest, networkProvisionFailureMessage } = await server.ssrLoadModule("@xgrids-k1/frontend/networkProvisioning.ts")); + ({ ApiError } = await server.ssrLoadModule("@xgrids-k1/frontend/api.ts")); +}); +after(async () => { await server?.close(); }); + +test("reviewed station errors explain Wi-Fi recovery without claiming a Bluetooth failure", () => { + for (const [code, pattern] of [ + ["k1-wifi-network-not-found", /точное название сети/], + ["k1-wifi-credentials-required", /введите пароль нужной сети заново/], + ]) { + const message = networkProvisionFailureMessage({ status: "failed", error: { code } }); + assert.match(message, pattern); + assert.doesNotMatch(message, /INVALID_PDU|ошибкой Bluetooth|Переподключиться/); + } + assert.match(networkProvisionFailureMessage({ status: "failed", error: { + code: "BleakGATTProtocolError", ble_att_error_code: 4, ble_att_error_name: "INVALID_PDU", + } }), /INVALID_PDU/); + assert.match(networkProvisionFailureMessage({ status: "failed", error: { + code: "network-not-found", + } }), /macOS/); +}); + +const request = { device_id: "synthetic-k1", connection_mode: "bridge", idempotency_key: "explicit-one" }; +function snapshot(status, revision = 2) { + return { + snapshot_runtime_id: "runtime-one", + snapshot_runtime_started_at_utc: "2026-09-06T00:00:00Z", + snapshot_revision: revision, + operations: status ? [{ + operation_id: "op-one", action: "network.provision", idempotency_key: request.idempotency_key, + status, error: status === "failed" ? { + code: "BleakGATTProtocolError", ble_att_error_code: 4, ble_att_error_name: "INVALID_PDU", + device_write_attempted: true, device_write_confirmed: false, + safe_to_retry: false, side_effect_status: "unknown", + } : null, + }] : [], + }; +} +function context(overrides = {}) { + let current = snapshot(null, 1); + const calls = []; + return { + calls, + initialState: current, + send: async (input) => { calls.push(["send", input]); throw new ApiError("transport", 502); }, + readState: async () => { calls.push(["read"]); return snapshot("failed"); }, + acceptState: (incoming) => { if (incoming.snapshot_revision >= current.snapshot_revision) current = incoming; }, + currentState: () => current, + assertCurrent() {}, + ...overrides, + }; +} + +test("HTTP 502 retains the exact failed snapshot and ATT facts after one submission", async () => { + const ctx = context(); + const result = await observeProvisioningRequest(request, ctx); + assert.deepEqual(ctx.calls.map(([kind]) => kind), ["send", "read"]); + assert.equal(ctx.calls[0][1], request); + assert.equal(result.operation.operation_id, "op-one"); + assert.equal(result.operation.error.ble_att_error_name, "INVALID_PDU"); + assert.equal(result.state.operations[0], result.operation); + assert.equal(result.transportError.status, 502); +}); + +test("an existing failed or successful intent is never resubmitted", async () => { + for (const status of ["failed", "succeeded"]) { + const ctx = context({ initialState: snapshot(status) }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.operation.status, status); + assert.equal(ctx.calls.length, 0); + } +}); + +test("lost HTTP response can observe journal success without another command", async () => { + const ctx = context({ readState: async () => snapshot("succeeded") }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.operation.status, "succeeded"); + assert.equal(ctx.calls.filter(([kind]) => kind === "send").length, 1); +}); + +test("an admitted pending intent is followed only by bounded local state reads", async () => { + let now = 0; + const ctx = context({ + initialState: snapshot("running"), + settlementOptions: { now: () => now, wait: async (ms) => { now += ms; } }, + }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.operation.status, "failed"); + assert.deepEqual(ctx.calls.map(([kind]) => kind), ["read"]); +}); + +test("newer WebSocket failure outranks a stale running REST result", async () => { + const newer = snapshot("failed", 7); + const ctx = context({ + send: async () => snapshot("running", 5), + currentState: () => newer, + }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.state, newer); + assert.equal(result.operation.status, "failed"); + assert.equal(ctx.calls.length, 0); +}); + +test("superseding an intent after dispatch stops continuation without replay", async () => { + let active = true; + const ctx = context({ + send: async () => { active = false; throw new ApiError("lost", 502); }, + assertCurrent: () => { if (!active) throw new Error("superseded"); }, + }); + await assert.rejects(observeProvisioningRequest(request, ctx), /superseded/); + assert.equal(ctx.calls.length, 0); +}); + +test("an unrelated journal row cannot confirm the current command", async () => { + const unrelated = snapshot("succeeded"); + unrelated.operations[0].idempotency_key = "another-intent"; + const ctx = context({ readState: async () => unrelated }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.operation, null); + assert.equal(result.transportError.status, 502); + assert.equal(ctx.calls.length, 1); +}); diff --git a/apps/control-station/test/observatoryWorkspace.test.mjs b/apps/control-station/test/observatoryWorkspace.test.mjs index ba50dc8..b1d0a3a 100644 --- a/apps/control-station/test/observatoryWorkspace.test.mjs +++ b/apps/control-station/test/observatoryWorkspace.test.mjs @@ -27,7 +27,7 @@ async function read(relativePath) { test("Observatory is the third independent Polygon workspace", () => { assert.deepEqual( productModel.workspacesForRoot("polygon").map(({ id }) => id), - ["lab-archive", "simulations", "observatory"], + ["lab-archive", "simulations", "observatory", "local-device"], ); assert.deepEqual( productModel.workspaceById("observatory"), @@ -138,7 +138,7 @@ test("Observatory mounts the one shared canonical replay only after explicit adm /RerunViewport|ObservationSessionSelect|resolveObservationSessionReplay|resolveCanonicalLabReplay|deleteObservationSession|setInterval|setTimeout|useObservationSessions|useAdvancedLaboratoryCatalog|advanced-index|prefetch|preload/, ); assert.equal(sharedReplay.match(/ id), - ["lab-archive", "simulations", "observatory"], + ["lab-archive", "simulations", "observatory", "local-device"], ); assert.equal( workspacesForRoot("system").some(({ id }) => id === "polygon-run"), diff --git a/apps/control-station/test/productShellContract.test.mjs b/apps/control-station/test/productShellContract.test.mjs index d3d8266..e3f86a8 100644 --- a/apps/control-station/test/productShellContract.test.mjs +++ b/apps/control-station/test/productShellContract.test.mjs @@ -29,7 +29,7 @@ test("top navigation has no Center and Park owns contour health first", () => { assert.equal(productModel.workspaceById("contour-health")?.root, "fleet"); assert.deepEqual( productModel.workspacesForRoot("polygon").map(({ id }) => id), - ["lab-archive", "simulations", "observatory"], + ["lab-archive", "simulations", "observatory", "local-device"], ); assert.equal(productModel.workspaceById("lab-archive")?.kind, "lab-archive"); assert.deepEqual( diff --git a/apps/control-station/test/viewerProfile.test.mjs b/apps/control-station/test/viewerProfile.test.mjs index bb3fef8..17a4ea1 100644 --- a/apps/control-station/test/viewerProfile.test.mjs +++ b/apps/control-station/test/viewerProfile.test.mjs @@ -7,6 +7,7 @@ let server; let LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE; let liveAcquisitionRerunProfile; let recordedSessionRerunProfile; +let laboratoryResultRerunProfile; before(async () => { server = await createServer({ @@ -18,6 +19,7 @@ before(async () => { LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE, liveAcquisitionRerunProfile, recordedSessionRerunProfile, + laboratoryResultRerunProfile, } = await server.ssrLoadModule("/src/core/observation/viewerProfile.ts")); }); @@ -86,3 +88,14 @@ test("the old LAB transport is fenced as explicit legacy comparison only", () => }); assert.equal(Object.isFrozen(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE), true); }); + +test("LAB has its own immutable result identity and discriminator", () => { + const input = {resultId:"E38-result-1",sourceUrl:"/shared.rrd",kind:"recorded-session",clock:"stream_time"}; + const lab = laboratoryResultRerunProfile(input); + assert.equal(lab.kind,"laboratory-result"); + assert.equal(lab.clock,"session_time"); + assert.equal(lab.resultId,"E38-result-1"); + assert.equal(recordedSessionRerunProfile({...input}).kind,"recorded-session"); + assert.equal(liveAcquisitionRerunProfile({...input}).kind,"live-acquisition"); + assert.throws(()=>laboratoryResultRerunProfile({...input,resultId:" "})); +}); diff --git a/apps/node-agent/cmd/node-agent/main.go b/apps/node-agent/cmd/node-agent/main.go index 68fa9f2..d9ecfd1 100644 --- a/apps/node-agent/cmd/node-agent/main.go +++ b/apps/node-agent/cmd/node-agent/main.go @@ -80,6 +80,12 @@ func run() error { return err } pairing.Sensors = app.Sensors + app.DeviceEnrollment, err = node.OpenDeviceEnrollment(*dir, nodeID) + if err != nil { + return err + } + pairing.DeviceEnrollment = app.DeviceEnrollment + app.Sensors.NetworkDevices = app.DeviceEnrollment app.Access = &node.AccessStore{Path: filepath.Join(*dir, "ssh-keys.json"), Users: func() []string { return node.LocalAdmins("/") }} if err := os.MkdirAll(filepath.Dir(*socket), 0700); err != nil { return err diff --git a/apps/node-agent/internal/node/device_enrollment.go b/apps/node-agent/internal/node/device_enrollment.go new file mode 100644 index 0000000..d95ca08 --- /dev/null +++ b/apps/node-agent/internal/node/device_enrollment.go @@ -0,0 +1,323 @@ +package node + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "os" + "path/filepath" + "sync" + "time" +) + +type EnrollmentCommand struct { + ID string `json:"operation_id"` + NodeID string `json:"node_id"` + RuntimeID string `json:"runtime_id"` + Action string `json:"action"` + Deadline string `json:"deadline_at"` + Parameters map[string]any `json:"parameters"` +} + +// This is the complete journal schema. No command parameters or credentials. +type EnrollmentOperation struct { + ID string `json:"operation_id"` + NodeID string `json:"node_id"` + RuntimeID string `json:"runtime_id"` + Action string `json:"action"` + State string `json:"state"` + Error string `json:"error,omitempty"` + Result map[string]any `json:"result,omitempty"` + Remote bool `json:"remote,omitempty"` + Updated int64 `json:"updated_at"` +} + +type DeviceEnrollment struct { + mu sync.Mutex + root string + nodeID string + client *http.Client + operations map[string]*EnrollmentOperation +} + +func OpenDeviceEnrollment(root, nodeID string) (*DeviceEnrollment, error) { + dir := filepath.Join(root, "device-enrollment") + if e := os.MkdirAll(dir, 0700); e != nil { + return nil, e + } + d := &DeviceEnrollment{root: dir, nodeID: nodeID, operations: map[string]*EnrollmentOperation{}} + d.client = &http.Client{Timeout: 185 * time.Second, Transport: &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", "/run/mission-core-k1/driver.sock") + }}} + files, _ := filepath.Glob(filepath.Join(dir, "op_*.json")) + for _, path := range files { + data, e := os.ReadFile(path) + if e != nil { + return nil, e + } + var op EnrollmentOperation + if json.Unmarshal(data, &op) != nil || !operationID.MatchString(op.ID) { + return nil, errors.New("invalid enrollment journal") + } + if op.State == "running" { + op.State = "unknown" + op.Error = "БК перезапущен. Обновите состояние K1 перед новой командой." + } + d.operations[op.ID] = &op + } + return d, nil +} + +func (d *DeviceEnrollment) call(ctx context.Context, path string, command any) (map[string]any, error) { + var body io.Reader + method := "GET" + if command != nil { + data, e := json.Marshal(command) + if e != nil { + return nil, e + } + body = bytes.NewReader(data) + method = "POST" + } + request, e := http.NewRequestWithContext(ctx, method, "http://k1"+path, body) + if e != nil { + return nil, e + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Node-Id", d.nodeID) + response, e := d.client.Do(request) + if e != nil { + return nil, errors.New("Служба K1 на БК недоступна.") + } + defer response.Body.Close() + var value map[string]any + if json.NewDecoder(io.LimitReader(response.Body, 65536)).Decode(&value) != nil || response.StatusCode != 200 { + return nil, errors.New("Действие K1 не подтверждено. Обновите состояние устройства.") + } + return value, nil +} + +func (d *DeviceEnrollment) Status() map[string]any { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + value, e := d.call(ctx, "/status", nil) + if e != nil { + value = map[string]any{"available": false} + } + value["node_id"] = d.nodeID + value["name"], _ = os.Hostname() + value["fresh"] = true + return value +} + +func (c EnrollmentCommand) valid(nodeID string) bool { + deadline, e := time.Parse(time.RFC3339Nano, c.Deadline) + if e != nil || !deadline.After(time.Now()) || time.Until(deadline) > 180*time.Second || c.NodeID != nodeID || !operationID.MatchString(c.ID) || c.RuntimeID == "" || len(c.RuntimeID) > 128 { + return false + } + keys := map[string]bool{} + switch c.Action { + case "scan", "networks": + case "connect", "verify": + keys = map[string]bool{"device_id": true, "discovery_generation": true, "mode_revision": true} + id, ok := c.Parameters["device_id"].(string) + if !ok || len(id) < 1 || len(id) > 128 { + return false + } + for _, key := range []string{"discovery_generation", "mode_revision"} { + v, ok := c.Parameters[key].(float64) + if !ok || v < 0 || v != float64(int64(v)) { + return false + } + } + if c.Action == "connect" { + keys["ssid"] = true + keys["password"] = true + for key, limit := range map[string]int{"ssid": 32, "password": 64} { + v, ok := c.Parameters[key].(string) + if !ok || len(v) < 1 || len(v) > limit { + return false + } + } + } + default: + return false + } + if len(keys) != len(c.Parameters) { + return false + } + for key := range c.Parameters { + if !keys[key] { + return false + } + } + return true +} + +func copyEnrollment(value *EnrollmentOperation) *EnrollmentOperation { + if value == nil { + return nil + } + data, _ := json.Marshal(value) + var out EnrollmentOperation + _ = json.Unmarshal(data, &out) + return &out +} + +func (d *DeviceEnrollment) Submit(c EnrollmentCommand, remote bool) (*EnrollmentOperation, error) { + if !c.valid(d.nodeID) { + return nil, errors.New("Проверьте БК, выбранное устройство и параметры сети.") + } + d.mu.Lock() + if old := d.operations[c.ID]; old != nil { + out := copyEnrollment(old) + d.mu.Unlock() + return out, nil + } + if len(d.operations) >= 2000 { + for id, value := range d.operations { + if value.State != "running" && time.Now().Unix()-value.Updated > 86400 { + if os.Remove(filepath.Join(d.root, id+".json")) == nil { + delete(d.operations, id) + } + } + } + if len(d.operations) >= 2000 { + d.mu.Unlock() + return nil, errors.New("Журнал подключений заполнен. Повторите позже.") + } + } + for _, v := range d.operations { + if v.State == "running" { + d.mu.Unlock() + return nil, errors.New("Дождитесь текущего действия K1.") + } + } + d.mu.Unlock() + if d.Status()["runtime_id"] != c.RuntimeID { + return nil, errors.New("Сеанс K1 изменился. Обновите устройства.") + } + d.mu.Lock() + // Status can yield; repeat admission under the lock before the durable write. + if old := d.operations[c.ID]; old != nil { + out := copyEnrollment(old) + d.mu.Unlock() + return out, nil + } + for _, v := range d.operations { + if v.State == "running" { + d.mu.Unlock() + return nil, errors.New("Дождитесь текущего действия K1.") + } + } + op := &EnrollmentOperation{ID: c.ID, NodeID: c.NodeID, RuntimeID: c.RuntimeID, Action: c.Action, State: "running", Remote: remote, Updated: time.Now().Unix()} + if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil { + d.mu.Unlock() + return nil, e + } + d.operations[c.ID] = op + out := copyEnrollment(op) + d.mu.Unlock() + go d.execute(c) + return out, nil +} + +func (d *DeviceEnrollment) execute(c EnrollmentCommand) { + deadline, _ := time.Parse(time.RFC3339Nano, c.Deadline) + ctx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + result, e := d.call(ctx, "/operation", c) + // Dropping references is best-effort lifetime reduction, not a claim that + // Go can zero every immutable JSON/string copy. No secret is journalled. + delete(c.Parameters, "password") + d.mu.Lock() + defer d.mu.Unlock() + op := d.operations[c.ID] + op.Updated = time.Now().Unix() + op.State = "complete" + op.Result = result + if e != nil { + op.State = "unknown" + op.Error = "Действие K1 не подтверждено. Обновите состояние; повторная команда автоматически не отправляется." + } + if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil { + op.State = "unknown" + op.Error = "Результат не сохранён. Обновите состояние K1." + } +} + +func (d *DeviceEnrollment) Get(id string) *EnrollmentOperation { + d.mu.Lock() + defer d.mu.Unlock() + return copyEnrollment(d.operations[id]) +} +func (d *DeviceEnrollment) RemoteResults() []*EnrollmentOperation { + d.mu.Lock() + defer d.mu.Unlock() + out := []*EnrollmentOperation{} + for _, op := range d.operations { + if op.Remote { + out = append(out, copyEnrollment(op)) + if len(out) == 8 { + break + } + } + } + return out +} +func (d *DeviceEnrollment) Acknowledge(ids []string) { + d.mu.Lock() + defer d.mu.Unlock() + for _, id := range ids { + if op := d.operations[id]; op != nil && op.State != "running" { + op.Remote = false + _ = savePrivateJSON(filepath.Join(d.root, id+".json"), op) + } + } +} + +func (d *DeviceEnrollment) Routes(mux *http.ServeMux, server *Server) { + mux.HandleFunc("GET /api/devices/enrollment", func(w http.ResponseWriter, r *http.Request) { + if !server.authorized(w, r) { + return + } + v := d.Status() + _, name := server.Store.Public() + v["name"] = name + reply(w, 200, v) + }) + mux.HandleFunc("POST /api/devices/enrollment/operations", func(w http.ResponseWriter, r *http.Request) { + if !server.authorized(w, r) { + return + } + r.Body = http.MaxBytesReader(w, r.Body, 16384) + var c EnrollmentCommand + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + if r.Header.Get("Content-Type") != "application/json" || decoder.Decode(&c) != nil { + reply(w, 400, map[string]string{"error": "Некорректный запрос подключения"}) + return + } + op, e := d.Submit(c, false) + if e != nil { + reply(w, 409, map[string]string{"error": e.Error()}) + return + } + reply(w, 202, op) + }) + mux.HandleFunc("GET /api/devices/enrollment/operations/{id}", func(w http.ResponseWriter, r *http.Request) { + if !server.authorized(w, r) { + return + } + op := d.Get(r.PathValue("id")) + if op == nil { + reply(w, 200, map[string]string{"state": "unknown", "error": "Результат недоступен. Обновите состояние K1."}) + return + } + reply(w, 200, op) + }) +} diff --git a/apps/node-agent/internal/node/device_enrollment_test.go b/apps/node-agent/internal/node/device_enrollment_test.go new file mode 100644 index 0000000..e9bd339 --- /dev/null +++ b/apps/node-agent/internal/node/device_enrollment_test.go @@ -0,0 +1,111 @@ +package node + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "sync/atomic" + "testing" + "time" +) + +type enrollmentHTTP func(*http.Request) (*http.Response, error) + +func (f enrollmentHTTP) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func enrollmentRequest() EnrollmentCommand { + return EnrollmentCommand{ID: "op_" + strings.Repeat("a", 32), NodeID: "node-test", RuntimeID: "runtime-test", Action: "connect", Deadline: time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), Parameters: map[string]any{"device_id": "test-ble", "mode_revision": float64(0), "discovery_generation": float64(1), "ssid": "test-network", "password": token()}} +} + +func TestEnrollmentJournalsNoCredentialAndDispatchesOnce(t *testing.T) { + root := t.TempDir() + d, e := OpenDeviceEnrollment(root, "node-test") + if e != nil { + t.Fatal(e) + } + var posts atomic.Int32 + d.client = &http.Client{Transport: enrollmentHTTP(func(r *http.Request) (*http.Response, error) { + body := `{"available":true,"runtime_id":"runtime-test"}` + if r.Method == "POST" { + posts.Add(1) + body = `{"available":true,"connected":true}` + } + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil + })} + c := enrollmentRequest() + secret := c.Parameters["password"].(string) + if _, e = d.Submit(c, true); e != nil { + t.Fatal(e) + } + deadline := time.Now().Add(time.Second) + for d.Get(c.ID).State == "running" && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + if d.Get(c.ID).State != "complete" { + t.Fatal("operation did not complete") + } + // A repeated delivery owns no new physical intent, even with a replaced secret. + c.Parameters["password"] = token() + if _, e = d.Submit(c, true); e != nil { + t.Fatal(e) + } + if posts.Load() != 1 { + t.Fatal("duplicate dispatch") + } + journal, e := os.ReadFile(filepath.Join(root, "device-enrollment", c.ID+".json")) + if e != nil { + t.Fatal(e) + } + results, _ := json.Marshal(d.RemoteResults()) + if bytes.Contains(journal, []byte(secret)) || bytes.Contains(results, []byte(secret)) || bytes.Contains(journal, []byte("parameters")) { + t.Fatal("credential/parameters entered journal or results") + } +} + +func TestEnrollmentRestartDoesNotReplayRunningIntent(t *testing.T) { + root := t.TempDir() + d, e := OpenDeviceEnrollment(root, "node-test") + if e != nil { + t.Fatal(e) + } + c := enrollmentRequest() + op := EnrollmentOperation{ID: c.ID, NodeID: c.NodeID, RuntimeID: c.RuntimeID, Action: c.Action, State: "running", Remote: true} + if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil { + t.Fatal(e) + } + restarted, e := OpenDeviceEnrollment(root, "node-test") + if e != nil { + t.Fatal(e) + } + if restarted.Get(c.ID).State != "unknown" { + t.Fatal("restart did not fence unknown result") + } + restarted.client = &http.Client{Transport: enrollmentHTTP(func(*http.Request) (*http.Response, error) { t.Fatal("restart contacted hardware"); return nil, nil })} + result, e := restarted.Submit(c, true) + if e != nil || result.State != "unknown" { + t.Fatal("repeated intent did not retain unknown outcome") + } +} + +func TestEnrollmentRejectsAnotherNodeExpiredAndHostMutation(t *testing.T) { + c := enrollmentRequest() + if !c.valid("node-test") { + t.Fatal("valid intent rejected") + } + if c.valid("another-node") { + t.Fatal("wrong node admitted") + } + c.Parameters["allow_host_wifi_switch"] = true + if c.valid("node-test") { + t.Fatal("host association admitted") + } + delete(c.Parameters, "allow_host_wifi_switch") + c.Deadline = time.Now().Add(-time.Second).Format(time.RFC3339Nano) + if c.valid("node-test") { + t.Fatal("expired operation admitted") + } +} diff --git a/apps/node-agent/internal/node/pairing.go b/apps/node-agent/internal/node/pairing.go index d188b74..e490a7f 100644 --- a/apps/node-agent/internal/node/pairing.go +++ b/apps/node-agent/internal/node/pairing.go @@ -39,20 +39,21 @@ type PairState struct { Revocations []CoreBinding `json:"revocations,omitempty"` } type Pairing struct { - Sensors *Sensors - mu sync.Mutex - path string - store *Store - state PairState - now func() time.Time - inventory func() Inventory - version string - lastSeen int64 - connection string - listenError string - failureWindow int64 - failures int - clients map[string]*http.Client + Sensors *Sensors + DeviceEnrollment *DeviceEnrollment + mu sync.Mutex + path string + store *Store + state PairState + now func() time.Time + inventory func() Inventory + version string + lastSeen int64 + connection string + listenError string + failureWindow int64 + failures int + clients map[string]*http.Client } func OpenPairing(store *Store, dir, version string, inventory func() Inventory) (*Pairing, error) { diff --git a/apps/node-agent/internal/node/pairing_transport.go b/apps/node-agent/internal/node/pairing_transport.go index e9348ab..f1a09bf 100644 --- a/apps/node-agent/internal/node/pairing_transport.go +++ b/apps/node-agent/internal/node/pairing_transport.go @@ -282,12 +282,28 @@ func (p *Pairing) channel(ctx context.Context) { payload["sensor_state"] = inv payload["sensor_results"] = p.Sensors.RemoteResults() } + if p.DeviceEnrollment != nil { + payload["device_enrollment"] = p.DeviceEnrollment.Status() + payload["enrollment_results"] = p.DeviceEnrollment.RemoteResults() + } result, status, e := p.send(ctx, *binding, "/v1/node/heartbeat", payload) p.mu.Lock() if p.state.Phase == "paired" && p.state.Binding != nil && p.state.Binding.BindingID == binding.BindingID { if e == nil && status == 200 { p.connection = "online" p.lastSeen = p.now().Unix() + if p.DeviceEnrollment != nil { + var ack []string + if json.Unmarshal(result["enrollment_acknowledgements"], &ack) == nil { + p.DeviceEnrollment.Acknowledge(ack) + } + var commands []EnrollmentCommand + if json.Unmarshal(result["enrollment_commands"], &commands) == nil { + for _, c := range commands { + _, _ = p.DeviceEnrollment.Submit(c, true) + } + } + } if p.Sensors != nil { var ack []string if json.Unmarshal(result["sensor_acknowledgements"], &ack) == nil { diff --git a/apps/node-agent/internal/node/sensors.go b/apps/node-agent/internal/node/sensors.go index 1cf1100..0e99d1c 100644 --- a/apps/node-agent/internal/node/sensors.go +++ b/apps/node-agent/internal/node/sensors.go @@ -45,19 +45,20 @@ type SensorOperation struct { Updated int64 `json:"updated_at"` } type Sensors struct { - events sensorEvents - mu sync.Mutex - prepareMu sync.Mutex - root string - nodeID string - instance string - client *http.Client - operations map[string]*SensorOperation - names map[string]string - initialized map[string]bool + events sensorEvents + mu sync.Mutex + prepareMu sync.Mutex + root string + nodeID string + instance string + client *http.Client + NetworkDevices *DeviceEnrollment + operations map[string]*SensorOperation + names map[string]string + initialized map[string]bool } -var sensorID = regexp.MustCompile(`^rsd455_[0-9a-f]{32}$`) +var sensorID = regexp.MustCompile(`^(rsd455|k1)_[0-9a-f]{32}$`) var operationID = regexp.MustCompile(`^op_[0-9a-f]{32}$`) func OpenSensors(root, nodeID string) (*Sensors, error) { @@ -168,6 +169,32 @@ func (s *Sensors) driver(path string, body any) (map[string]any, error) { func (s *Sensors) Inventory() map[string]any { items := []any{} seen := map[string]bool{} + if s.NetworkDevices != nil { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + result, e := s.NetworkDevices.call(ctx, "/inventory", nil) + cancel() + if e == nil { + if found, ok := result["items"].([]any); ok { + for _, raw := range found { + item, ok := raw.(map[string]any) + if !ok { + continue + } + id, _ := item["id"].(string) + if !strings.HasPrefix(id, "k1_") || !sensorID.MatchString(id) { + continue + } + s.mu.Lock() + if name := s.names[id]; name != "" { + item["name"] = name + } + s.mu.Unlock() + seen[id] = true + items = append(items, item) + } + } + } + } if result, e := s.driver("/inventory", nil); e == nil { if found, ok := result["items"].([]any); ok { for _, v := range found { @@ -259,6 +286,9 @@ func sensorViewAction(action string) bool { } func (s *Sensors) Submit(c SensorCommand, remote bool) (*SensorOperation, error) { + if strings.HasPrefix(c.Session.DeviceID, "k1_") && (c.Action == "prepare" || c.Action == "replay") { + return nil, errors.New("Эта операция не поддерживается K1.") + } if c.APIVersion != SensorSchema || c.Kind != "OperationRequest" || !operationID.MatchString(c.ID) || c.Idempotency != c.ID || !sensorID.MatchString(c.Session.DeviceID) || len(c.Session.SessionID) > 192 { return nil, errors.New("Некорректная команда устройства.") } @@ -344,7 +374,14 @@ func (s *Sensors) execute(c SensorCommand) { } } else { var v map[string]any - v, err = s.driver("/operation", c) + if strings.HasPrefix(c.Session.DeviceID, "k1_") && s.NetworkDevices != nil { + deadline, _ := time.Parse(time.RFC3339Nano, c.Deadline) + ctx, cancel := context.WithDeadline(context.Background(), deadline) + v, err = s.NetworkDevices.call(ctx, "/sensor-operation", c) + cancel() + } else { + v, err = s.driver("/operation", c) + } uncertain = err != nil || v["state"] == "unknown" if err == nil { if v["state"] == "complete" { diff --git a/apps/node-agent/internal/node/server.go b/apps/node-agent/internal/node/server.go index c56e04f..819ddca 100644 --- a/apps/node-agent/internal/node/server.go +++ b/apps/node-agent/internal/node/server.go @@ -13,20 +13,21 @@ import ( ) type Server struct { - Store *Store - Pairing *Pairing - Sensors *Sensors - Assets fs.FS - Origin string - Version string - Inventory func() Inventory - Access *AccessStore - Tailscale func() TailscaleStatus - Environment func() EnvironmentStatus - mu sync.Mutex - logins map[string]time.Time - sessions map[string]time.Time - Now func() time.Time + Store *Store + Pairing *Pairing + Sensors *Sensors + DeviceEnrollment *DeviceEnrollment + Assets fs.FS + Origin string + Version string + Inventory func() Inventory + Access *AccessStore + Tailscale func() TailscaleStatus + Environment func() EnvironmentStatus + mu sync.Mutex + logins map[string]time.Time + sessions map[string]time.Time + Now func() time.Time } func token() string { @@ -82,6 +83,9 @@ func (s *Server) Handler() http.Handler { if s.Sensors != nil { s.Sensors.Routes(mux, s) } + if s.DeviceEnrollment != nil { + s.DeviceEnrollment.Routes(mux, s) + } if s.Pairing != nil { s.Pairing.localRoutes(mux, s) } diff --git a/apps/node-agent/packaging/50-mission-core-k1.rules b/apps/node-agent/packaging/50-mission-core-k1.rules new file mode 100644 index 0000000..a16bc55 --- /dev/null +++ b/apps/node-agent/packaging/50-mission-core-k1.rules @@ -0,0 +1,6 @@ +// Only WLAN discovery. No host association/profile modification authority. +polkit.addRule(function(action, subject) { + if (subject.user === "mission-core-k1" && action.id === "org.freedesktop.NetworkManager.wifi.scan") { + return polkit.Result.YES; + } +}); diff --git a/apps/node-agent/packaging/build.py b/apps/node-agent/packaging/build.py index 71984fc..ff09a55 100644 --- a/apps/node-agent/packaging/build.py +++ b/apps/node-agent/packaging/build.py @@ -11,7 +11,7 @@ import sys from build_deb import build, VERSION, BRAND_SHA256 ROOT = Path(__file__).resolve().parents[1] -DG_COMMIT = "999864e5b0a81555823cfa1ea6e8cf8a417c37f1" +DG_COMMIT = "1bdfc6c24072d38cc1068086ea271c444f2524ad" def guideline_sources(): @@ -32,6 +32,8 @@ def provenance(): "base_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(), "design_guideline_commit": DG_COMMIT, "design_guideline_files": guideline_sources(), + "shared_sensor_ui_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "packages/sensor-ui/src").rglob("*")) if p.is_file()}, + "k1_runtime_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "src/k1link").rglob("*")) if p.is_file() and p.suffix in (".py", ".json")}, "toolchain": json.loads((ROOT / "toolchain.json").read_text()), "files": files} diff --git a/apps/node-agent/packaging/build_deb.py b/apps/node-agent/packaging/build_deb.py index 1d0138f..bbccd8c 100644 --- a/apps/node-agent/packaging/build_deb.py +++ b/apps/node-agent/packaging/build_deb.py @@ -13,7 +13,7 @@ import tarfile ROOT = Path(__file__).resolve().parents[1] -VERSION = "0.6.11" +VERSION = "0.7.0" BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af" @@ -65,7 +65,7 @@ Architecture: amd64 Maintainer: NODE.DC local build Section: admin Priority: optional -Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme +Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, bluez, network-manager, iproute2, ffmpeg Description: Mission Core onboard computer configuration Local graphical setup, host inventory, SSH access and persistent node identity. """.encode() @@ -107,6 +107,29 @@ Description: Mission Core onboard computer configuration files.append(("usr/share/mission-core-node/realsense/" + item["name"], data, 0o644)) for path in (ROOT / "sensors").glob("*.py"): files.append(("usr/lib/mission-core-node/sensors/" + path.name, path.read_bytes(), 0o644)) + for name in ("k1_prepare.py", "k1_bootstrap.py"): + files.append(("usr/lib/mission-core-node/" + name, (p / name).read_bytes(), 0o644)) + files.append(("usr/lib/mission-core-node/install-k1-credential", (p / "install-k1-credential").read_bytes(), 0o755)) + files.append(("usr/lib/systemd/system/mission-core-k1.service", (p / "mission-core-k1.service").read_bytes(), 0o644)) + files.append(("usr/share/polkit-1/rules.d/50-mission-core-k1.rules", (p / "50-mission-core-k1.rules").read_bytes(), 0o644)) + k1_bundle = json.loads((p / "k1-bundle.json").read_text()) + files.append(("usr/share/mission-core-node/k1/bundle.json", (p / "k1-bundle.json").read_bytes(), 0o644)) + for item in k1_bundle["wheels"]: + data = (ROOT / "build/k1-wheels" / item["name"]).read_bytes() + if hashlib.sha256(data).hexdigest() != item["sha256"]: + raise ValueError("K1 bundle hash mismatch") + files.append(("usr/share/mission-core-node/k1/" + item["name"], data, 0o644)) + repository = ROOT.parents[1] + # Reuse the admitted plugin runtime and the transport-neutral renderer. + # No separate Core web service is started on the board. + for path in (repository / "src/k1link").rglob("*"): + if path.is_file() and path.suffix in (".py", ".json"): + files.append(("usr/lib/mission-core-node/k1/src/k1link/" + str(path.relative_to(repository / "src/k1link")), path.read_bytes(), 0o644)) + for relative in ("plugins/xgrids-k1/profile_loader.py", "plugins/xgrids-k1/plugin.manifest.json", + "config/observatory-equipment-models.json", + "config/observatory-recorded-capture-profiles.json", + "plugins/xgrids-k1/profiles/fw-3.0.2/local-network.v2.json"): + files.append(("usr/lib/mission-core-node/k1/" + relative, (repository / relative).read_bytes(), 0o644)) sdk = ROOT.parents[1] / "packages/plugin-sdk/python/missioncore_plugin_sdk" for path in sdk.rglob("*.py"): files.append(("usr/lib/mission-core-node/sdk/missioncore_plugin_sdk/" + str(path.relative_to(sdk)), path.read_bytes(), 0o644)) diff --git a/apps/node-agent/packaging/fetch_driver_bundle.py b/apps/node-agent/packaging/fetch_driver_bundle.py index a55fa23..be66b17 100644 --- a/apps/node-agent/packaging/fetch_driver_bundle.py +++ b/apps/node-agent/packaging/fetch_driver_bundle.py @@ -1,13 +1,18 @@ """Engineering build input, never run on an operator board. Exact PyPI hashes only.""" import hashlib +import argparse import json +import time from pathlib import Path -from urllib.request import urlopen +from urllib.request import Request, urlopen root = Path(__file__).resolve().parents[1] -manifest = json.loads((root / "packaging/realsense-bundle.json").read_text()) -output = root / "build/realsense-wheels" +parser = argparse.ArgumentParser() +parser.add_argument("--model", choices=("realsense", "k1"), default="realsense") +model = parser.parse_args().model +manifest = json.loads((root / f"packaging/{model}-bundle.json").read_text()) +output = root / f"build/{model}-wheels" output.mkdir(parents=True, exist_ok=True) for item in manifest["wheels"]: target = output / item["name"] @@ -23,8 +28,24 @@ for item in manifest["wheels"]: ) if not source["url"].startswith("https://files.pythonhosted.org/"): raise ValueError("Unexpected package origin") - with urlopen(source["url"], timeout=120) as response: - data = response.read(item["bytes"] + 1) + partial = target.with_suffix(target.suffix + ".partial") + for attempt in range(4): + offset = partial.stat().st_size if partial.exists() else 0 + request = Request(source["url"], headers={"Range": f"bytes={offset}-"} if offset else {}) + try: + with urlopen(request, timeout=60) as response: + if offset and response.status != 206: + raise RuntimeError("Package server did not honor resume range") + with partial.open("ab" if offset else "wb") as stream: + while chunk := response.read(1024 * 1024): + stream.write(chunk) + break + except (OSError, TimeoutError): + if attempt == 3: + raise + time.sleep(2) + data = partial.read_bytes() if len(data) != item["bytes"] or hashlib.sha256(data).hexdigest() != item["sha256"]: raise ValueError("Driver checksum mismatch") - target.write_bytes(data) + partial.replace(target) + print(json.dumps({"model": model, "wheel": item["name"], "bytes": len(data)}), flush=True) diff --git a/apps/node-agent/packaging/install-k1-credential b/apps/node-agent/packaging/install-k1-credential new file mode 100644 index 0000000..137501b --- /dev/null +++ b/apps/node-agent/packaging/install-k1-credential @@ -0,0 +1,36 @@ +#!/usr/bin/python3 -I +"""Administrator-only import of the exact application key from protected stdin.""" +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +if os.geteuid() != 0 or len(sys.argv) != 1: + raise SystemExit("Root stdin import required") +secret = bytearray(sys.stdin.buffer.read(1025).strip()) +try: + if len(secret) != 36 or any(v < 33 or v > 126 for v in secret): + raise SystemExit("Credential does not match the reviewed K1 profile") + root = Path("/etc/credstore.encrypted") + root.mkdir(mode=0o700, exist_ok=True) + if root.is_symlink() or root.stat().st_uid != 0 or root.stat().st_mode & 0o022: + raise SystemExit("Unsafe credential store") + path = root / "k1-application" + if path.is_symlink() or path.exists(): + raise SystemExit("K1 credential already installed; explicit rotation required") + with tempfile.TemporaryDirectory(prefix=".k1-", dir=root) as directory: + staged = Path(directory) / "encrypted" + completed = subprocess.run( + ["/usr/bin/systemd-creds", "encrypt", "--name=k1-application", "--with-key=host", "-", str(staged)], + input=secret, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30, + ) + if completed.returncode: + raise SystemExit("K1 credential import failed") + staged.chmod(0o600) + with staged.open("rb") as stream: + os.fsync(stream.fileno()) + # Atomic publication without overwriting a concurrently installed key. + os.link(staged, path) +finally: + secret[:] = b"\0" * len(secret) diff --git a/apps/node-agent/packaging/k1-bundle.json b/apps/node-agent/packaging/k1-bundle.json new file mode 100644 index 0000000..c2b5a83 --- /dev/null +++ b/apps/node-agent/packaging/k1-bundle.json @@ -0,0 +1,265 @@ +{ + "schema": "missioncore.node.driver-bundle/v1", + "model_id": "xgrids.k1", + "revision": "c7ed0bba39f757afdac176a8", + "python": "3.12", + "platform": "linux-amd64", + "lock_sha256": "551c8ccdc44bc3724328dd1e316c81d20e63d2e4dcd97148377d6cacef479246", + "wheels": [ + { + "name": "aioice-0.10.2-py3-none-any.whl", + "sha256": "14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf", + "bytes": 24875 + }, + { + "name": "aiortc-1.14.0-py3-none-any.whl", + "sha256": "4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e", + "bytes": 93183 + }, + { + "name": "annotated_doc-0.0.4-py3-none-any.whl", + "sha256": "571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", + "bytes": 5303 + }, + { + "name": "annotated_types-0.7.0-py3-none-any.whl", + "sha256": "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "bytes": 13643 + }, + { + "name": "anyio-4.14.2-py3-none-any.whl", + "sha256": "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", + "bytes": 125813 + }, + { + "name": "attrs-26.1.0-py3-none-any.whl", + "sha256": "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", + "bytes": 67548 + }, + { + "name": "av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", + "sha256": "7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", + "bytes": 41174337 + }, + { + "name": "bleak-3.0.2-py3-none-any.whl", + "sha256": "39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d", + "bytes": 146490 + }, + { + "name": "certifi-2026.7.22-py3-none-any.whl", + "sha256": "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", + "bytes": 136983 + }, + { + "name": "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "sha256": "c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", + "bytes": 221822 + }, + { + "name": "click-8.4.2-py3-none-any.whl", + "sha256": "e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", + "bytes": 119243 + }, + { + "name": "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", + "sha256": "42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", + "bytes": 4459756 + }, + { + "name": "dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9", + "bytes": 852687 + }, + { + "name": "dnspython-2.8.0-py3-none-any.whl", + "sha256": "01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", + "bytes": 331094 + }, + { + "name": "fastapi-0.139.0-py3-none-any.whl", + "sha256": "cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", + "bytes": 130339 + }, + { + "name": "foxglove_sdk-0.25.3-cp310-abi3-manylinux_2_28_x86_64.whl", + "sha256": "bcc894b88188d8169973cfbb1370300f671760adea9d6e9447e5a03b2289527d", + "bytes": 19220466 + }, + { + "name": "google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", + "sha256": "14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", + "bytes": 33364 + }, + { + "name": "h11-0.16.0-py3-none-any.whl", + "sha256": "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", + "bytes": 37515 + }, + { + "name": "httpcore-1.0.9-py3-none-any.whl", + "sha256": "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", + "bytes": 78784 + }, + { + "name": "httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "sha256": "b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", + "bytes": 523851 + }, + { + "name": "httpx-0.28.1-py3-none-any.whl", + "sha256": "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", + "bytes": 73517 + }, + { + "name": "idna-3.18-py3-none-any.whl", + "sha256": "7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", + "bytes": 65455 + }, + { + "name": "ifaddr-0.2.0-py3-none-any.whl", + "sha256": "085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", + "bytes": 12314 + }, + { + "name": "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", + "bytes": 1368249 + }, + { + "name": "markdown_it_py-4.2.0-py3-none-any.whl", + "sha256": "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", + "bytes": 91687 + }, + { + "name": "mdurl-0.1.2-py3-none-any.whl", + "sha256": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "bytes": 9979 + }, + { + "name": "numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", + "bytes": 16672469 + }, + { + "name": "paho_mqtt-2.1.0-py3-none-any.whl", + "sha256": "6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", + "bytes": 67219 + }, + { + "name": "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", + "bytes": 6940830 + }, + { + "name": "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", + "bytes": 155560 + }, + { + "name": "pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", + "sha256": "5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", + "bytes": 50088993 + }, + { + "name": "pycparser-3.0-py3-none-any.whl", + "sha256": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", + "bytes": 48172 + }, + { + "name": "pydantic-2.13.4-py3-none-any.whl", + "sha256": "45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", + "bytes": 472262 + }, + { + "name": "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "sha256": "926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", + "bytes": 2094516 + }, + { + "name": "pyee-14.0.0-py3-none-any.whl", + "sha256": "3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d", + "bytes": 15553 + }, + { + "name": "pygments-2.20.0-py3-none-any.whl", + "sha256": "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", + "bytes": 1231151 + }, + { + "name": "pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc", + "bytes": 2434534 + }, + { + "name": "pyopenssl-26.2.0-py3-none-any.whl", + "sha256": "4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", + "bytes": 55823 + }, + { + "name": "python_dotenv-1.2.2-py3-none-any.whl", + "sha256": "1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", + "bytes": 22101 + }, + { + "name": "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", + "bytes": 807870 + }, + { + "name": "rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_x86_64.whl", + "sha256": "287059b7154bf3881f5b32035f5772d0556d55a0a894650fb74a2605fb39afbe", + "bytes": 163018185 + }, + { + "name": "rich-14.3.4-py3-none-any.whl", + "sha256": "07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", + "bytes": 310480 + }, + { + "name": "shellingham-1.5.4-py2.py3-none-any.whl", + "sha256": "7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", + "bytes": 9755 + }, + { + "name": "starlette-1.3.1-py3-none-any.whl", + "sha256": "c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", + "bytes": 73632 + }, + { + "name": "typer-0.26.8-py3-none-any.whl", + "sha256": "3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", + "bytes": 122564 + }, + { + "name": "typing_extensions-4.16.0-py3-none-any.whl", + "sha256": "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", + "bytes": 45571 + }, + { + "name": "typing_inspection-0.4.2-py3-none-any.whl", + "sha256": "4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", + "bytes": 14611 + }, + { + "name": "uvicorn-0.51.0-py3-none-any.whl", + "sha256": "5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", + "bytes": 73219 + }, + { + "name": "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", + "bytes": 4426307 + }, + { + "name": "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "sha256": "e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", + "bytes": 456398 + }, + { + "name": "websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "sha256": "35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a", + "bytes": 187345 + } + ] +} diff --git a/apps/node-agent/packaging/k1_bootstrap.py b/apps/node-agent/packaging/k1_bootstrap.py new file mode 100644 index 0000000..eddb271 --- /dev/null +++ b/apps/node-agent/packaging/k1_bootstrap.py @@ -0,0 +1,26 @@ +"""Fixed root-owned import path; no user site, environment path or import hooks.""" + +import os +import sys +from pathlib import Path + +root = Path("/var/lib/mission-core-k1-runtime") +reference = root / "active.path" +runtime = Path(reference.read_text().strip()) +if ( + reference.is_symlink() + or runtime.is_symlink() + or runtime.parent != root + or not runtime.name.isalnum() + or runtime.stat().st_uid != 0 + or runtime.stat().st_mode & 0o022 +): + raise RuntimeError("Unsafe K1 runtime") +sys.path[:0] = [ + str(runtime), str(runtime / "rerun_sdk"), + "/usr/lib/mission-core-node/k1/src", "/usr/lib/mission-core-node/sdk", +] +os.environ["MISSIONCORE_DATA_DIR"] = "/var/lib/mission-core-k1" +from k1link.device_plugins.xgrids_k1.node_bridge import main + +main() diff --git a/apps/node-agent/packaging/k1_prepare.py b/apps/node-agent/packaging/k1_prepare.py new file mode 100644 index 0000000..0b8db58 --- /dev/null +++ b/apps/node-agent/packaging/k1_prepare.py @@ -0,0 +1,94 @@ +"""Install only the bundled, hash-pinned Ubuntu K1 runtime; no network I/O.""" + +import hashlib +import json +import os +import shutil +import sys +import tempfile +import zipfile +from pathlib import Path, PurePosixPath + +SHARE = Path("/usr/share/mission-core-node/k1") +ROOT = Path("/var/lib/mission-core-k1-runtime") + + +def members(archive): + for info in archive.infolist(): + path = PurePosixPath(info.filename) + if ( + path.is_absolute() + or ".." in path.parts + or (info.external_attr >> 16) & 0o170000 == 0o120000 + or ".data" in path.parts + ): + raise RuntimeError("Unsafe K1 runtime archive") + # Rerun's pinned wheel declares this one static package directory. + # We do not execute .pth files; bootstrap adds the exact directory. + if info.filename.endswith(".pth") and not ( + info.filename == "rerun_sdk.pth" and archive.read(info) == b"rerun_sdk\n" + ): + raise RuntimeError("Unreviewed K1 Python path hook") + # Distribution script/data relocation must be handled deliberately, + # never interpreted as an install hook by the operator's Python. + if any(part.endswith(".data") for part in path.parts): + raise RuntimeError("K1 wheel requires unsupported relocation") + yield info + + +def prepare(): + if os.geteuid() != 0 or os.uname().machine != "x86_64" or sys.version_info[:2] != (3, 12): + raise RuntimeError("K1 runtime requires privileged Ubuntu amd64 Python 3.12 installation") + release = Path("/etc/os-release").read_text() + if "ID=ubuntu" not in release or 'VERSION_ID="24.04"' not in release: + raise RuntimeError("K1 runtime requires Ubuntu 24.04") + manifest = json.loads((SHARE / "bundle.json").read_text()) + revision = manifest["revision"] + if not revision.isalnum(): + raise RuntimeError("Invalid K1 runtime revision") + ROOT.mkdir(mode=0o755, exist_ok=True) + if ROOT.is_symlink() or ROOT.stat().st_uid != 0 or ROOT.stat().st_mode & 0o022: + raise RuntimeError("Unsafe K1 runtime root") + target = ROOT / revision + if target.is_symlink() or (ROOT / "active.path").is_symlink(): + raise RuntimeError("Unsafe K1 runtime reference") + for item in manifest["wheels"]: + path = SHARE / item["name"] + if ( + path.name != item["name"] + or path.is_symlink() + or hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"] + ): + raise RuntimeError("K1 runtime checksum mismatch") + if not target.exists(): + stage = Path(tempfile.mkdtemp(prefix=".k1-", dir=ROOT)) + try: + for item in manifest["wheels"]: + with zipfile.ZipFile(SHARE / item["name"]) as archive: + archive.extractall(stage, members=members(archive)) + for path in stage.rglob("*"): + path.chmod(0o755 if path.is_dir() else 0o644) + stage.chmod(0o755) + stage.rename(target) + finally: + if stage.exists(): + shutil.rmtree(stage) + for item in manifest["wheels"]: + with zipfile.ZipFile(SHARE / item["name"]) as archive: + for info in members(archive): + path = target / info.filename + if path.is_symlink() or ( + not info.is_dir() and path.read_bytes() != archive.read(info) + ): + raise RuntimeError("Installed K1 runtime differs from bundled wheel") + fd, name = tempfile.mkstemp(prefix=".active-", dir=ROOT) + with os.fdopen(fd, "w") as stream: + os.fchmod(stream.fileno(), 0o644) + stream.write(str(target)) + stream.flush() + os.fsync(stream.fileno()) + os.replace(name, ROOT / "active.path") + + +if __name__ == "__main__": + prepare() diff --git a/apps/node-agent/packaging/mission-core-k1.service b/apps/node-agent/packaging/mission-core-k1.service new file mode 100644 index 0000000..a077f42 --- /dev/null +++ b/apps/node-agent/packaging/mission-core-k1.service @@ -0,0 +1,38 @@ +[Unit] +Description=Mission Core Node K1 Bridge and acquisition +After=bluetooth.service NetworkManager.service +Wants=bluetooth.service NetworkManager.service + +[Service] +Type=simple +User=mission-core-k1 +Group=mission-core-node +SupplementaryGroups=bluetooth +ExecStart=/usr/bin/python3 -I /usr/lib/mission-core-node/k1_bootstrap.py +StateDirectory=mission-core-k1 +StateDirectoryMode=0700 +RuntimeDirectory=mission-core-k1 +RuntimeDirectoryMode=0750 +LoadCredentialEncrypted=k1-application +UMask=0007 +Environment=OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 +Restart=on-failure +RestartSec=3 +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +CapabilityBoundingSet= +LockPersonality=yes +TasksMax=128 +MemoryMax=2G +LimitNOFILE=2048 + +[Install] +WantedBy=multi-user.target diff --git a/apps/node-agent/packaging/postinst b/apps/node-agent/packaging/postinst index 1bb0274..cacd52a 100644 --- a/apps/node-agent/packaging/postinst +++ b/apps/node-agent/packaging/postinst @@ -8,6 +8,10 @@ case "$1" in if ! getent passwd mission-core-sensors >/dev/null; then adduser --system --group --home /var/lib/mission-core-sensors --no-create-home --disabled-login mission-core-sensors fi + if ! getent passwd mission-core-k1 >/dev/null; then + adduser --system --home /var/lib/mission-core-k1 --no-create-home --disabled-login --ingroup mission-core-node mission-core-k1 + fi + /usr/bin/python3 -I /usr/lib/mission-core-node/k1_prepare.py # Only bootstrap required to open the GUI. Operational configuration is a # versioned job started by «Настройка окружения → Сконфигурировать». if [ -d /run/systemd/system ]; then @@ -15,6 +19,8 @@ case "$1" in systemctl enable mission-core-node.service systemctl restart mission-core-node.service systemctl try-restart mission-core-realsense.service + systemctl enable mission-core-k1.service + systemctl restart mission-core-k1.service fi ;; esac diff --git a/apps/node-agent/packaging/preinst b/apps/node-agent/packaging/preinst index b49cb8d..cff2e9e 100644 --- a/apps/node-agent/packaging/preinst +++ b/apps/node-agent/packaging/preinst @@ -8,6 +8,12 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then exit 1 fi fi + if [ -S /run/mission-core-k1/driver.sock ]; then + if ! /usr/bin/python3 -I -c 'import http.client,json,socket; c=http.client.HTTPConnection("k1",timeout=5); c.sock=socket.socket(socket.AF_UNIX); c.sock.settimeout(5); c.sock.connect("/run/mission-core-k1/driver.sock"); c.request("GET","/prepare-safe"); r=c.getresponse(); assert r.status==200 and json.load(r).get("safe") is True'; then + echo "Mission Core Node: завершите подключение или запись K1 перед обновлением." >&2 + exit 1 + fi + fi mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true) case "$mc_node_device_job" in active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;; @@ -20,6 +26,11 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then exit 1 ;; esac + # End the admitted idle worker before dpkg replaces its Python modules. + # New UI commands now fail unavailable instead of racing the package copy. + if [ -f /usr/lib/systemd/system/mission-core-k1.service ]; then + systemctl stop mission-core-k1.service + fi fi . /etc/os-release if [ "${ID:-}" != ubuntu ] || [ "${VERSION_ID:-}" != 24.04 ]; then diff --git a/apps/node-agent/packaging/prerm b/apps/node-agent/packaging/prerm index 08d783d..da15f13 100644 --- a/apps/node-agent/packaging/prerm +++ b/apps/node-agent/packaging/prerm @@ -7,6 +7,12 @@ if [ -d /run/systemd/system ]; then exit 1 fi fi + if [ -S /run/mission-core-k1/driver.sock ]; then + if ! /usr/bin/python3 -I -c 'import http.client,json,socket; c=http.client.HTTPConnection("k1",timeout=5); c.sock=socket.socket(socket.AF_UNIX); c.sock.settimeout(5); c.sock.connect("/run/mission-core-k1/driver.sock"); c.request("GET","/prepare-safe"); r=c.getresponse(); assert r.status==200 and json.load(r).get("safe") is True'; then + echo "Mission Core Node: завершите подключение или запись K1 перед обновлением." >&2 + exit 1 + fi + fi mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true) case "$mc_node_device_job" in active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;; @@ -40,6 +46,8 @@ case "$1" in /usr/sbin/sshd -t systemctl try-reload-or-restart ssh.service fi + systemctl stop mission-core-k1.service + systemctl disable mission-core-k1.service || true systemctl stop mission-core-realsense.service systemctl disable mission-core-realsense.service || true systemctl stop mission-core-node.service diff --git a/apps/node-agent/packaging/resolve_k1_bundle.py b/apps/node-agent/packaging/resolve_k1_bundle.py new file mode 100644 index 0000000..f5828df --- /dev/null +++ b/apps/node-agent/packaging/resolve_k1_bundle.py @@ -0,0 +1,60 @@ +"""Resolve Linux wheel inputs from the frozen monorepo lock, without downloading.""" +import hashlib +import json +import subprocess +import tomllib +from pathlib import Path + +from packaging.markers import default_environment +from packaging.requirements import Requirement +from packaging.tags import compatible_tags, cpython_tags +from packaging.utils import canonicalize_name, parse_wheel_filename + +ROOT = Path(__file__).resolve().parents[1] +REPOSITORY = ROOT.parents[1] + + +def resolve(): + requirements = ROOT / "build/k1-requirements.txt" + requirements.parent.mkdir(exist_ok=True) + subprocess.run(["uv", "export", "--frozen", "--extra", "node-device-media", "--no-dev", + "--no-emit-project", "--no-emit-package", "missioncore-plugin-sdk", "--no-hashes", + "--output-file", str(requirements)], cwd=REPOSITORY, check=True, stdout=subprocess.DEVNULL) + environment = default_environment() + environment.update(sys_platform="linux", platform_system="Linux", platform_machine="x86_64", + python_version="3.12", python_full_version="3.12.3", implementation_name="cpython", + platform_python_implementation="CPython") + platforms = [f"manylinux_2_{n}_x86_64" for n in range(39, 16, -1)] + ["manylinux2014_x86_64", "linux_x86_64"] + tags = list(cpython_tags((3, 12), platforms=platforms)) + list(compatible_tags((3, 12), interpreter="cp312", platforms=platforms)) + ranks = {tag: i for i, tag in enumerate(tags)} + lock_data = (REPOSITORY / "uv.lock").read_bytes() + lock = tomllib.loads(lock_data.decode()) + items = [] + for line in requirements.read_text().splitlines(): + if not line or line.lstrip().startswith("#"): + continue + requirement = Requirement(line) + if requirement.marker and not requirement.marker.evaluate(environment): + continue + name = canonicalize_name(requirement.name) + package = next(v for v in lock["package"] if canonicalize_name(v["name"]) == name and v["version"] in requirement.specifier) + candidates = [] + for wheel in package.get("wheels", []): + filename = wheel["url"].split("/")[-1] + _, _, _, wheel_tags = parse_wheel_filename(filename) + matches = [ranks[tag] for tag in wheel_tags if tag in ranks] + if matches: + candidates.append((min(matches), filename, wheel)) + if not candidates: + raise RuntimeError("No reviewed Linux wheel: " + name) + _, filename, wheel = min(candidates) + items.append({"name": filename, "sha256": wheel["hash"].removeprefix("sha256:"), "bytes": wheel["size"]}) + revision = hashlib.sha256(json.dumps(items, sort_keys=True).encode()).hexdigest()[:24] + manifest = {"schema": "missioncore.node.driver-bundle/v1", "model_id": "xgrids.k1", "revision": revision, + "python": "3.12", "platform": "linux-amd64", "lock_sha256": hashlib.sha256(lock_data).hexdigest(), "wheels": items} + (ROOT / "packaging/k1-bundle.json").write_text(json.dumps(manifest, indent=2) + "\n") + print(json.dumps({"wheels": len(items), "bytes": sum(v["bytes"] for v in items), "revision": revision})) + + +if __name__ == "__main__": + resolve() diff --git a/apps/node-agent/ui/package-lock.json b/apps/node-agent/ui/package-lock.json index 6c318f5..a4c22df 100644 --- a/apps/node-agent/ui/package-lock.json +++ b/apps/node-agent/ui/package-lock.json @@ -11,6 +11,7 @@ "@nodedc/tokens": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/tokens", "@nodedc/ui-core": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core", "@nodedc/ui-react": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", + "@rerun-io/web-viewer": "0.36.3", "react": "19.1.0", "react-dom": "19.1.0" }, @@ -18,7 +19,8 @@ "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "typescript": "^5.8.3", - "vite": "^7.0.0" + "vite": "^7.0.0", + "vite-plugin-wasm": "^3.6.0" } }, "../../../../NODEDC_DESIGN_GUIDELINE/packages/tokens": { @@ -524,6 +526,12 @@ "resolved": "../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", "link": true }, + "node_modules/@rerun-io/web-viewer": { + "version": "0.36.3", + "resolved": "https://registry.npmjs.org/@rerun-io/web-viewer/-/web-viewer-0.36.3.tgz", + "integrity": "sha512-LMGnsxRmY5UwiGras2dZrMnEYkow5Xr4v+1hAUSspXWPPiilMqoz9G77jo8Ps/deAaX81TnOq123DFO8iX/Ulw==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.63.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", @@ -1239,6 +1247,16 @@ "optional": true } } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz", + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } } } } diff --git a/apps/node-agent/ui/package.json b/apps/node-agent/ui/package.json index 1c77b70..a1d20c7 100644 --- a/apps/node-agent/ui/package.json +++ b/apps/node-agent/ui/package.json @@ -9,6 +9,7 @@ "build": "tsc --noEmit && vite build" }, "dependencies": { + "@rerun-io/web-viewer": "0.36.3", "@nodedc/ui-react": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", "@nodedc/ui-core": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core", "@nodedc/tokens": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/tokens", @@ -16,6 +17,7 @@ "react-dom": "19.1.0" }, "devDependencies": { + "vite-plugin-wasm": "^3.6.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "typescript": "^5.8.3", diff --git a/apps/node-agent/ui/rerun-runtime.html b/apps/node-agent/ui/rerun-runtime.html new file mode 100644 index 0000000..08963b8 --- /dev/null +++ b/apps/node-agent/ui/rerun-runtime.html @@ -0,0 +1,16 @@ + + + + + + Живой просмотр K1 + + + +
+ + + diff --git a/apps/node-agent/ui/src/NodeSensors.tsx b/apps/node-agent/ui/src/NodeSensors.tsx index 26294e1..8842b3a 100644 --- a/apps/node-agent/ui/src/NodeSensors.tsx +++ b/apps/node-agent/ui/src/NodeSensors.tsx @@ -1,5 +1,6 @@ +import {createIsolatedRerunHost} from '../../../control-station/src/components/rerun/isolatedRerunHost'; import {SensorWorkspace} from '../../../../packages/sensor-ui/src/SensorWorkspace'; import type {SensorTransport} from '../../../../packages/sensor-ui/src/contracts'; import {request} from './api'; -const transport:SensorTransport={subscribe:(receive,unavailable)=>{const events=new EventSource('/api/devices/events');events.onmessage=e=>{try{receive(JSON.parse(e.data));}catch{unavailable();}};events.onerror=unavailable;return()=>events.close();},inventory:()=>request('/api/devices'),submit:value=>request('/api/devices/operations','POST',value),operation:id=>request('/api/devices/operations/'+encodeURIComponent(id))}; -export function NodeSensors(){return ;} +const transport:SensorTransport={enrollment:{state:()=>request("/api/devices/enrollment"),submit:value=>request("/api/devices/enrollment/operations","POST",value),operation:id=>request("/api/devices/enrollment/operations/"+encodeURIComponent(id))},subscribe:(receive,unavailable)=>{const events=new EventSource('/api/devices/events');events.onmessage=e=>{try{receive(JSON.parse(e.data));}catch{unavailable();}};events.onerror=unavailable;return()=>events.close();},inventory:()=>request('/api/devices'),submit:value=>request('/api/devices/operations','POST',value),operation:id=>request('/api/devices/operations/'+encodeURIComponent(id))}; +export function NodeSensors(){return ;} diff --git a/apps/node-agent/ui/src/rerunRuntime.ts b/apps/node-agent/ui/src/rerunRuntime.ts new file mode 100644 index 0000000..2753a41 --- /dev/null +++ b/apps/node-agent/ui/src/rerunRuntime.ts @@ -0,0 +1 @@ +import "../../../control-station/src/components/rerun/isolatedRerunEntry"; diff --git a/apps/node-agent/ui/vite.config.js b/apps/node-agent/ui/vite.config.js index 6c16b4f..17c5972 100644 --- a/apps/node-agent/ui/vite.config.js +++ b/apps/node-agent/ui/vite.config.js @@ -1,9 +1,17 @@ import { defineConfig } from "vite"; +import { fileURLToPath } from "node:url"; +import wasm from "vite-plugin-wasm"; export default defineConfig({ + plugins: [wasm()], // Shared sensor TSX lives outside this app tsconfig; use the same JSX runtime. esbuild: { jsx: "automatic" }, // Design Guideline packages are linked during development. Their own React // must never become a second hook dispatcher in the portable production bundle. resolve: { dedupe: ["react", "react-dom", "@nodedc/ui-react"] }, + optimizeDeps: { exclude: ["@rerun-io/web-viewer"] }, + build: { target: "esnext", rollupOptions: { input: { + app: fileURLToPath(new URL("./index.html", import.meta.url)), + rerun: fileURLToPath(new URL("./rerun-runtime.html", import.meta.url)), + } } }, }); diff --git a/docs/04_K1_WIFI_PROVISIONING_PROFILE.md b/docs/04_K1_WIFI_PROVISIONING_PROFILE.md index ce193bb..aa9f5d3 100644 --- a/docs/04_K1_WIFI_PROVISIONING_PROFILE.md +++ b/docs/04_K1_WIFI_PROVISIONING_PROFILE.md @@ -171,6 +171,25 @@ mode. It never fragments or retries the payload automatically. ## Expected transition and evidence of acceptance +### Firmware station error replies + +Offline review of the exact 3.0.2 `lixel_nman` callback found that station +`wifi_connect` returns are passed directly to the 7f01 GATT write result: +4 denotes its SSID-not-found paths and 6 denotes its credentials-required path. +Generic Bluetooth libraries label the same values INVALID_PDU and +REQUEST_NOT_SUPPORTED. Preserve those raw values but do not discard the +reviewed device interpretation. The evidence and disassembly addresses are in +[the callback audit](audits/2026-09-06-k1-station-reply-semantics.md). + +Apply this interpretation only to the selected exact firmware station profile +and its annotated 99-byte write-with-response failure. It does not apply to +Quick Connect, a failed baseline read or arbitrary ATT errors. It grants no +retry, network-state confirmation or ledger resolution. The firmware performs +Wi-Fi work before completing this callback; a delayed reply alone is not proof +that the BLE connection request was the slow stage. + +### Acceptance observations + A completed GATT write only proves transport completion. It does not prove that the K1 joined Wi-Fi or began beaconing. The application polls `7f02`; the observed response frame contains a fixed-width text slot, an address slot, a diff --git a/docs/audits/2026-09-06-k1-bridge-connection-incident.md b/docs/audits/2026-09-06-k1-bridge-connection-incident.md new file mode 100644 index 0000000..95c6f9d --- /dev/null +++ b/docs/audits/2026-09-06-k1-bridge-connection-incident.md @@ -0,0 +1,140 @@ +# K1 Bridge: provisioning recovery and first-scan discovery + +Scope: the operator-local K1 connection in LAB on canonical Core `8000`. +The onboard installation is a separate pending acceptance task. + +The later [firmware callback audit](2026-09-06-k1-station-reply-semantics.md) +explains the two fresh 19:22/19:23 UTC failures and supersedes the earlier +generic ATT interpretation below with a bounded FW 3.0.2 station diagnosis. + +## Evidence and diagnosis + +Three explicit Bridge attempts returned HTTP 502 on 2026-09-06 at +18:04:50, 18:05:13 and 18:05:43 UTC. All failed at `gatt-write` with +`BleakGATTProtocolError`, ATT 4 `INVALID_PDU`. Each attempted one 99-byte +write-with-response; acknowledgement and a joined Wi-Fi address were absent. +The characteristic advertised `read, write`; the observed write-without-response +capacity was 253 bytes. This capacity alone does not prove a successful ATT write. + +A separately admitted, exact-target Bridge `connection.verify` at 18:15 UTC +read K1 state without a provisioning write or host Wi-Fi switch. K1 answered, +but supplied no shared-LAN address (`connection-verify-address-unavailable`). +A preceding request was rejected at input validation because of the diagnostic +operation identifier format; it performed no device I/O. + +The same ATT error exists in August's private logs. The accepted August Bridge +record uses the same Bleak 3.0.2, write mode, frame length and capacity. Successful +historical writes also include an empty network baseline, so an empty baseline +does not justify adding an AP-enable command or rejecting Bridge in advance. +The exact reason for the peripheral's rejection remains unproved; a wrong Wi-Fi +password, frame-format regression or MTU failure must not be asserted from this +error alone. No alternate transport mode, frame, retry, START or STOP was sent. + +Private evidence is retained under the ignored incident directory, with UTC and +monotonic timestamps, redacted events and SHA-256 artifact index. Credentials and +raw packet contents are absent from this report and source changes. + +## Software causes and refactor + +- The Connect hook had three overlapping result/error branches. Its failed + HTTP path accepted the server snapshot but returned `observedState: null` to + the form; the form therefore lost the exact operation and its ATT diagnosis. +- A completed Bluetooth search suppressed the recovery surface even after the + subsequent, click-owned Connect failed. The disabled credential form instead + promised a future safe continuation despite there being no running operation. +- Recovery guidance claimed an automatic recovery or a completed UI reset that + had not occurred. The lead status fell back to idle despite a terminal failure. +- Durable JSON logging omitted the write-mode, frame-size and GATT-property + fields already supplied by the BLE implementation. + +`networkProvisioning.ts` now owns the single submission, bounded observation of +that exact idempotency key, monotonic snapshot selection and reviewed failure +copy. An existing operation is only observed, never resubmitted. The runtime +hook retains exact applied-network/control-authority checks and React action +ownership. The form renders the returned operation and allows explicit recovery +after its failed attempt even when its preceding search is complete. It still +clears the credential immediately upon submission. + +The frozen BLE frames and canonical MQTT dialogue are unchanged. Ops reference: +MISSIONCOR-3, “Mission Core. Lixel K1 / XGRIDS Integration”, and +`docs/lab/002_LIXELGO_IPHONE_LOCAL_PROTOCOL_20260716.redacted.md`. +That iPhone capture begins at the IP stack and does not contain Bluetooth HCI. +The reviewed BLE profile remains `docs/04_K1_WIFI_PROVISIONING_PROFILE.md`. + +## Validation and remaining acceptance + +Focused frontend: 227 passed, including new behavioral cases for HTTP 502, +existing-key non-replay, lost-response success, pending settlement, stale REST +versus newer WebSocket, superseded intent and unrelated journal rows. The +rendered regression covers completed search followed by failed Connect and +requires enabled recovery choices instead of the trapped form. + +Full frontend suite: 772 passed sequentially; TypeScript and final production build passed. Application architecture: 4 passed. + +BLE and persistent-diagnostic tests: 34 passed. Canonical guardrail: four +immutable LixelGO captures verified in their original checkout, frozen +protocol/Bridge contour unchanged, 32 synthetic sentinels passed. The convenience +script initially failed because raw captures are deliberately absent from the +active worktree; its original-location integrity check passed without copying +or changing the captures. Ruff and diff checks passed. + +A later runtime journal records an explicit successful Bridge write at 18:42 UTC, +followed by application-control acceptance, canonical START and the first point +frame, then a confirmed STOP at 18:44 UTC. These actions occurred while the +assistant was editing/testing discovery; they were not dispatched by this +investigation. The success followed the recovery-UI refactor and preceded the +new discovery implementation. It proves a subsequent successful physical +connection, not the cause or permanent resolution of the intermittent ATT error. +Do not automatically replay the earlier failed attempts. + +## First-scan miss + +The operator reported that an already active K1 is absent from the first search +and appears on the second. This was reproduced on the original six-second +implementation: 18:32 UTC returned four devices and zero K1 candidates; a second +explicit scan at 18:35 UTC returned five devices including the expected K1. +These observations do not distinguish radio advertisement latency from macOS +state and do not establish that the camera was powered off. + +Discovery now opens one native Bleak scanner context. It listens for an initial +six-second window and, if no K1 name has appeared, continues the same context +up to a twenty-second bound, stopping on a later K1 candidate. There is no hidden +second scan, GATT connection, provisioning retry or cache-based candidate +promotion. Name matching only ends discovery; compatibility and connection +still require their separate evidence. The owner arbiter, generation revocation, +native-handle capture and cancellation cleanup remain in place. A caller's +explicit shorter duration is respected. + +The shared frontend request/countdown, backend default and Node Bridge source +use the twenty-second bound. Completed operations and private structured logs +now include scanner startup, total elapsed time, first-candidate time and whether +the initial window was extended. They contain no Wi-Fi credentials. + +After the new canonical process started, the first explicit UI search at +18:51 UTC found the expected K1. Native startup took 766 ms, first K1 detection +2023 ms from scanner construction, and total discovery 6768 ms. Extension was +not needed. The browser showed the fresh candidate and the arbiter returned idle. +This is one successful process-restart test; the browser cache was not cleared. +The operator requested a separate test after browser-cache clearing. That +acceptance is pending: the available in-app browser automation exposes no cache +clearing capability, and the clear-browsing-data keyboard shortcut had no effect. +The operator was asked to clear it; no cache clearing is claimed. + +Additional discovery validation: 73 backend tests passed (scanner, owner arbiter, +persistent diagnostics and Node Bridge), seven facade discovery tests passed, +138 focused frontend tests passed, and architecture checks passed. TypeScript +and production build passed. Final focused discovery checks, Ruff, whitespace +and the frozen protocol/Bridge comparison passed. The previously completed full +772-test frontend pass applies to the recovery refactor; only affected suites +were rerun for the subsequent discovery change. + +Canonical Core remains on port 8000; no listener exists on 8765. Onboard source +is synchronized separately; this investigation does not install or rebuild the +pending Node package. + +## Ops publication + +The existing Ops canon was read. Automatic approval review rejected the attempted +card update because consultation was authorized but publication of internal +technical details was not. No Ops card was changed; this local report is the +reviewable result pending explicit publication authorization. diff --git a/docs/audits/2026-09-06-k1-live-reference-camera-root.md b/docs/audits/2026-09-06-k1-live-reference-camera-root.md new file mode 100644 index 0000000..fad5052 --- /dev/null +++ b/docs/audits/2026-09-06-k1-live-reference-camera-root.md @@ -0,0 +1,129 @@ +# K1 live reference comparison and missing camera + +Scope: operator-local K1 Bridge data acquisition on canonical Mission Core 8000. +No new physical START, STOP, provisioning or RTSP request was sent during this +investigation. The comparison uses the existing 6 September capture. + +## Authoritative references found in Ops + +MISSIONCOR-3, “Mission Core. Lixel K1 / XGRIDS Integration”, block “Внутренние +эталоны Mission Core” and its comment dated 22 August 2026 identify: + +- Fast reference A: eaad9de, 20260822T105904Z_viewer_live. START to calibration + under 5 seconds; calibration 21 seconds; after calibration cloud 2 seconds, + right camera 4 seconds; STOP to physical onset 1 second. +- Recovery reference B: 1001a31, 20260822T130323Z_viewer_live. START 14 seconds; + calibration 22 seconds; cloud +1 second, camera +8 seconds; STOP onset 7 seconds. + +The linked canonical report is +[Lab 010](../lab/010_K1_MISSION_CORE_INTERNAL_LIVE_BASELINE_20260822.redacted.md). +These are measured internal references for one K1 FW 3.0.2 and Bridge/direct LAN, +not a transferable SLA or a standalone Rerun preset. A used a short physical +ledger; B retained mature recovery history. Source quality was not reduced. + +MISSIONCOR-66, “Mission Core. Канон интеграции Rerun”, and MISSIONCOR-74, +“Additional Core · Переносимая кастомизация Rerun”, define the separate +presentation contracts: + +| Surface | Current profile | Clock and media | +| --- | --- | --- | +| Live acquisition | live-acquisition | stream_time; upstream live Rerun; right camera through the independent RTSP → durable fMP4 → MSE path | +| Saved Sessions / Data | recorded-session | session_time; progressive recorded admission; RecordedFmp4Player follows the shared playback clock | +| Canonical LAB result | laboratory-result | session_time; result-specific settings; merged RRD with native AssetVideo/VideoFrameReference; separate presentation gate | + +The live receiver does not inherit the LAB full-readiness gate. Changing point +size, accumulation or blueprint cannot repair a camera producer that never +started. The current live display settings include accumulation 47 seconds, +point size 1 and height/viridis; no evidence identifies those visual values as +part of the fast reference, so they were not arbitrarily reset. + +### Settings isolation limitation found during the audit + +Distinct profile kinds, clocks and media admission do not prove complete +settings isolation. App.tsx still owns one sceneSettings/displayDraft pair for +live and Data, and useWorkspaceLayoutProfile() loads and saves one +observation.spatial profile containing scene settings. The normal settings +committer suppresses backend writes while recorded replay is presented, but it +still updates the common in-memory settings. The persisted layout restore/apply +path also has no profile-kind namespace. LAB uses its own resultId-scoped draft +and durable view profile. + +Therefore this audit confirms distinct presentation contracts and LAB settings +ownership, not full live/Data settings isolation. No shared settings, layout, +Rerun renderer or replay code was changed for this camera repair. The focused +profile tests below do not cover the remaining live/Data settings coupling. +Separating that storage and state requires its own transition/race and browser +regression checks; it must not be folded into a camera-path fix implicitly. + +## Observed 6 September failure + +Existing session 20260906T184240Z_viewer_live: + +| Metric | Fast A | Recovery B | Recent run | +| --- | ---: | ---: | ---: | +| MQTT callback → publication p50 | 23.839 ms | 83.934 ms | 93.151 ms | +| MQTT callback → publication p95 | 41.541 ms | 223.589 ms | 208.989 ms | +| Preview drops | 0 | 70 | 58 | +| Point decode errors | 0 | 0 | 0 | +| Camera archive | complete | complete | absent | + +Run lengths differ; drop counts are not normalized performance rates. Device +calibration onset and first visible pixels were not independently measured in +the recent run, so the historical operator timings are not falsely compared to +backend timestamps. The recent run published 1,028,061 points in 386 PCL frames. + +At 18:43:15 UTC the browser admitted a Rerun store; this alone is not proof of +visible point pixels. Between 18:43:20 and 18:43:50 the backend logged 22 failed +post-authoritative-PCL camera activations. First-PCL admission took 4–34 ms. +No camera producer activation success or camera media artifact exists in this +session. The private formatter discarded exception details, preventing recovery +of each historical exception stack from those records. + +## Reproduced storage defect and bounded repair + +The running checkout is separate from MISSIONCORE_DATA_DIR. Acquisition uses +resolve_missioncore_evidence_dir(), but XgridsK1CameraGateway previously confined +session paths to repository_root. The actual session is outside the checkout. +An offline call using the real existing session directory deterministically +raised “camera recording root must stay inside the repository” before authority +reservation, FFmpeg preparation or network I/O. Camera remained idle, matching +the observed pre-producer failure. This mismatch necessarily blocks recording +at the configured path even though the original exception stacks were lost. + +The gateway now receives an explicit evidence_root from the existing service +composition. It confines both acquisition-owned and selected-preview recording +to that root after resolving paths. It rejects sibling directories and symlink +escapes. The source checkout remains the FFmpeg-binary lookup root; the fallback +for standalone gateway callers preserves their existing repository confinement. +No RTSP arguments, video quality, stream choice, camera producer lifecycle, +START/STOP, MQTT dialogue, Rerun blueprint or LAB/archive viewer policy changed. + +Private exception diagnostics now retain only the exception class and final +filename/line/function. They omit exception text, locals, source lines and +absolute paths. This makes future failures attributable without leaking data. + +## Validation and remaining physical acceptance + +- Camera gateway suite: 42 passed, including seven new external-root, + composition-wiring and path-confinement cases. Synthetic FFmpeg produced + and archived media in the configured external directory. +- Camera acquisition lifecycle: 37 passed. +- Persistent diagnostics suite: 8 passed, including exception-location redaction. +- Focused frontend profile, environment, LAB view profile and recorded-camera + journal checks: 14 passed. These are contract-level checks, not physical + playback acceptance or proof of complete settings isolation. +- Mypy on camera.py and runtime_diagnostics.py: passed. +- Ruff and git diff --check: passed. +- Protocol, BLE provisioning/AP, physical ledger/coordinator, MQTT, + connection supervisor, runtime and archive remain identical to c041a56. +- The broad frozen-contour guard also includes camera.py, so it now deliberately + detects this narrow camera storage change. Its baseline was not advanced or + weakened. This is not a claim that the full freeze check passes unchanged. + +The canonical idle service was refreshed and its replacement was confirmed +ready on port 8000 at 19:10:22 UTC; no frontend rebuild was required. A new +operator-started physical run is still +needed to verify right-camera appearance, durable media and browser playback. +The existing failed session is preserved and is not retroactively repaired. + +Ops was consulted read-only. This local report was not published to a card. diff --git a/docs/audits/2026-09-06-k1-station-reply-semantics.md b/docs/audits/2026-09-06-k1-station-reply-semantics.md new file mode 100644 index 0000000..b7ad82f --- /dev/null +++ b/docs/audits/2026-09-06-k1-station-reply-semantics.md @@ -0,0 +1,95 @@ +# K1 station replies: misleading ATT error and fresh failures + +Scope: the two operator-started Bridge attempts on Core 8000 following the +operator-reported browser cache clear. No device write, START/STOP, Wi-Fi switch, +subnet scan or firmware execution was performed by this investigation. + +## Fresh evidence + +| Attempt, UTC | K1 first advertisement | Complete scan | Connect operation | Reply | +| --- | ---: | ---: | ---: | --- | +| 19:22:25 | 1.308 s | 6.366 s | 51.399 s | ATT 4 INVALID_PDU | +| 19:23:55 | 2.208 s | 6.023 s | 7.587 s | ATT 4 INVALID_PDU | + +Both attempts reached the validated GATT contract, baseline status read and +one 99-byte write-with-response. Neither write was acknowledged as successful. +The observed command capacity was 253 bytes. Empty passwords are rejected +before this path; presence does not prove correctness. Individual connect and +write durations were not recorded, so total latency is not falsely attributed +entirely to the Wi-Fi operation or entirely to CoreBluetooth. + +Private snapshots, operation identities and the disassembly are retained under +the ignored `.runtime/k1-connect-incident-20260906/fresh-1922/` directory with +0600 files, UTC/monotonic timestamps and a SHA-256 artifact index. + +## Firmware-level explanation + +The reviewed official FW 3.0.2 artifact is the immutable source documented in +[Lab 004](../lab/004_K1_FW302_AP_CREDENTIAL_PROVIDER_20260720.redacted.md). +Its extracted `lixel_nman` executable is 323,464 bytes, SHA-256 +`aead745e4e0073ae99e84e851e5560161f2d560d5804f21e5268116efbb1dc42`, +ELF build ID `205fb546e44667ca0e74159a7672269a81b754f5`. + +Bounded offline AArch64 disassembly established: + +1. At 0x14588 the service registers characteristic 7f01, with write callback + 0x17e48 selected at 0x14598. +2. Its station branch calls `wifi_connect` at 0x18034 → 0x15440. The return + value is retained in w28 at 0x18038 and passed as the write-result error at + 0x17fc4 → 0x291a8. +3. `wifi_connect` returns 4 in its network-not-found branches: 0x16190 and + 0x16a2c. Adjacent diagnostics refer explicitly to the SSID not being found. +4. The branch matching NetworkManager's credentials-required output returns + 6 at 0x1641c. This describes an unsuccessful credential path, not proof + of the exact incorrect character or how the form was populated. + +Thus the device's application return codes collide with generic ATT names: +4 is displayed as INVALID_PDU and 6 as REQUEST_NOT_SUPPORTED. The existing +Mission Core message discarded the reviewed device meaning. Neither changing +the 99-byte layout nor switching write mode follows from these observations. +The callback performs Wi-Fi work before returning, so its response is not merely +an instantaneous transport acknowledgment. + +This is an interpretation under the selected exact firmware profile. It is not +a universal ATT error mapping, proof of the live firmware before DeviceInfo, +or confirmation that the network configuration remained unchanged. The actual +SSID/radio condition still needs a corrected operator-run connection test. +The old screenshot displays a network name whose exact spelling was queried; +this report does not assume that spelling is a typo or assume K1 can see it. + +## Bounded correction + +`wifi_failure.py` owns the pure profile-scoped classification. The service uses +it only for Bridge/Direct, selected FW 3.0.2, an annotated gatt-write failure, +a dispatched/unconfirmed 99-byte write-with-response and BleakGATTProtocolError. +It retains the original exception code and raw ATT facts. Quick Connect, +baseline-read failures, other firmware and unrelated errors retain their +original diagnostics. Retry flags, mutation ledger and ambiguity remain intact. + +The frontend shares the station guidance between the form and recovery panel. +It offers “Указать сеть Wi-Fi заново” through the existing explicit reset and +scan flow. The pending stage explains K1 Wi-Fi connection. It does not populate, +store, inspect or change the password. The GATT helper, packet bytes, MQTT, +physical command control and all Rerun profiles are unchanged in this increment. + +## Validation + +- 19 focused backend cases passed: 16 classification boundary cases, two + service-level reply cases and the existing ambiguous-write recovery case. + They verify one submission, retained raw codes, failed/unconfirmed state and + an unresolved ledger rather than promoting an error to success. +- 24 classification/diagnostic cases passed; mypy and Ruff passed. +- 775 frontend tests passed, including rendered station guidance and the + explicit same-device network setup action. Architecture: 4 passed. +- Production typecheck and build passed. The replacement canonical Core was + confirmed ready at 2026-09-06T19:41:10.665304+00:00. +- Browser check: the real Core 8000 Test Devices page loaded; normal and expanded + sizes worked. The page was left open. No BLE action was clicked. The new error + messages were covered by rendered tests, not represented as a new hardware run. +- Hardware Wi-Fi acceptance remains pending the exact target SSID and an + operator-started attempt. macOS networksetup did not provide the current SSID; + its output was not treated as proof that the host network is down. + +Ops MISSIONCOR-3 was read through the direct MCP. Its iPhone network capture +starts at IP and does not contain Bluetooth HCI; it cannot establish the meaning +of this GATT callback. No Ops card was written. diff --git a/docs/audits/2026-09-06-node-k1-bridge-implementation.md b/docs/audits/2026-09-06-node-k1-bridge-implementation.md new file mode 100644 index 0000000..ea7a346 --- /dev/null +++ b/docs/audits/2026-09-06-node-k1-bridge-implementation.md @@ -0,0 +1,164 @@ +# K1 Bridge on the onboard computer + +Authority: the owner's 2026-09-06 request and two annotated Fleet screenshots. +The earlier Node context is historical evidence, not a new instruction. X4 work +is paused while its battery charges. Hardware acceptance below remains pending +until the owner powers on K1 and supplies the target WLAN in the application. + +## Product surface decision + +The operator occasionally adds a wireless sensor to one selected onboard +computer. The primary entity is that computer's device inventory. Discovery, +network observations, credentials and device control belong to that computer; +the operator's Mission Core is a paired remote console. Node's local console +uses the same form and backend contract. + +Selected composition: a plus beside refresh in the device inventory opens the +canonical modal Window, titled «Подключение устройства к БК». The target board +is visible before discovery or credentials. K1 is selected from an explicit +scan on that board. The operator chooses a board-observed WLAN or enters an +SSID, then submits one Bridge operation. Success requires observed device +connectivity; dispatch alone is never shown as connected. + +An independent Fleet connection workspace was considered: it separates the +action from its board and can imply operator-local radios. A new primary root +was also unnecessary. The owner explicitly selected the inventory plus and +authorized moving the existing operator-local connection workspace into LAB, +with all its current modes and functions, under «Тестовые устройства». +The stable workspace ID is retained for saved navigation. + +This is domain content in admitted list/detail and modal compositions. Reuse +`Window`, `WindowFooterActions`, `TextField` (including password), `Select`, +`Button`, `IconButton`, `ResourceRow`, `SettingsCard`, `StatusBadge`, +`ActivityIndicator`, `ToastStack`; icons `plus`, `refresh`, `network`, `camera`, +`eye`, `settings`, `close`, `play`, `stop`. These exist in the sibling Design +Guideline registries. No new generic visual entity is introduced. + +State grammar: board unavailable; service unavailable; ready to scan; +scanning; no candidates; candidate selected; network list unavailable with +manual entry; ready to connect; connecting; observed connected; failed before +write; outcome unknown after possible write. A stale discovery/runtime or +changed board invalidates the form. Closing the modal clears its password. +Closing after dispatch does not claim cancellation of a physical operation. + +## Execution and security boundaries + +Only Bridge is admitted on Node. Reuse the reviewed firmware 3.0.2 profile and +99-byte 7f01 operation with 7f02 observation and exact endpoint verification. +No Quick Connect, host association, AP enable, subnet scan or firmware action +is exposed by the Node adapter. BLE notifications, where used by the existing +profile, can entail the standard temporary CCCD subscription write. + +Use the existing authenticated Node/Core channel. A device-enrollment command +targets the paired node and a worker runtime/discovery generation, even before +a device session exists. Wi-Fi credentials must not enter the existing durable +sensor-command journal, Fleet database, error text, evidence or argv. Pending +secret payloads are short-lived memory only. A restart, expired command or +uncertain dispatch never retries a provisioning write with a new identity. +The Node worker is the sole hardware owner for both consoles. + +Linux adapters must observe BlueZ and NetworkManager on the board. Host-route +verification uses the kernel route to the exact K1 address; a tunnel/default +route cannot silently qualify as the required local Bridge path. Existing +macOS adapter behavior and the accepted local LAB workflows are retained. + +## Rerun profile boundary + +| Profile | Source / clock | Settings authority | Lifetime | +| --- | --- | --- | --- | +| Live acquisition | Current board camera/LiDAR / stream_time | Live preview settings | Active acquisition and execution binding | +| Recorded session | Immutable admitted recording / session_time | Session replay, trajectory, time and playback settings | Recording identity | +| Laboratory result | Immutable admitted result and recording / session_time | Result-specific scene, evidence layers, diagnostic selections | Result identity | + +The native renderer and recorded data pipeline can be shared, but each profile +has an explicit discriminator. Crossing profiles remounts the renderer so its +refs, subscriptions and pending recovery cannot leak into another profile. +LAB uses its own factory and result identity. Source recording/application IDs +remain unchanged: profile separation does not rewrite evidence lineage. + +The Node publishes native RRD through ordered WebRTC data channels and the +existing camera gateway publishes H.264/fMP4 through a second channel. Signalling +uses paired sensor operations; ICE admits private LAN/Tailscale host candidates +only, with no STUN/TURN. The Node Rerun sink opens no gRPC port. Both hosts inject +the existing isolated native Rerun renderer into the shared sensor UI. + +Two viewers at most are admitted. Decoded preview envelopes use latest-value +queues; encoded RRD bytes are never dropped within an open recording. Slow +consumers are closed. Camera preview leases share the canonical recording +producer. Closing a viewer releases its peer and delivery lease; acquisition +STOP remains a separate explicit operator action. These bounds concern delivery; +sustained native-viewer CPU/GPU/memory acceptance requires the real board test. + +Node live settings (point size, accumulation, color, palette, points, trajectory, +grid) invoke only `viewer.settings.update` behind `profile=live-acquisition`. +Recorded and LAB profiles keep their own controls. LAB result changes remount +the renderer as well as changes between the three profile kinds. + +## Validation and current acceptance + +- Focused backend tests: board binding, expiration, replay prevention, secret + non-persistence, restart/unknown outcomes and Linux route classifications. +- Architecture gate; frontend typecheck, unit tests and build sequentially. +- Node package build and installation provenance; service health after reboot. +- Browser: both plus controls, target board, scan/empty/error states, manual + SSID, password clearing, keyboard Escape, normal/expanded live viewer. +- Real K1: one explicit scan/select/Bridge, actual DeviceInfo verification, + device appears in both inventories; camera and LiDAR acquisition/stop; + reconnect and restart; regression of local LAB and recorded replay. + +No hardware acceptance or successful deployment is claimed by this document. + +## Completed validation + +- Core architecture gate: 4/4; complete frontend suite: 764/764 after updating + navigation expectations. Core and Node typechecks and production builds pass. +- Node Go tests pass, including redacted enrollment journal, one dispatch per + intent, restart outcome unknown, wrong node/expired request/host mutation deny. +- Focused Python Fleet, pairing, Node SDK, existing BLE scanner and Rerun tests + pass (one existing optional case skipped). Empty Node adapter construction, + public state and teardown pass locally, with no hardware discovery/listener. +- Six WebRTC tests pass, including an actual bounded loopback data-channel + roundtrip. Missing camera preserves the RRD channel; rejected offers release + the peer. Native RRD sink emits an RRF2 header without opening gRPC. +- All 51 Linux wheel hashes and archive members were checked. The only admitted + .pth is Rerun's literal package-directory declaration; bootstrap adds that + exact directory without executing path hooks. Ubuntu 24.04/amd64/Python 3.12 + and fixed root-owned runtime paths are enforced. Installation checks idle + acquisition and stops the old K1 worker before replacing its modules. +- Canonical Core checkout was updated and its exact launch agent restarted. + `/api/liveness` and the new enrollment API pass; the paired board remains + online. Exactly one integrated backend remains on 8000, none on 8765. +- In-app browser: Fleet no longer contains the old connection workspace; LAB + opens Test devices with the existing K1 scenario. The vehicle inventory plus + opens the board-scoped modal. Unavailable-service state and Escape were + verified against the real paired board, which still runs the prior Node. + +## Package and remaining work + +Prepared package: `apps/node-agent/build/mission-core-node_0.7.0_amd64.deb`. +Size: 429540264 bytes. SHA-256: +`10b1164b57c31cd3edcced8653faf14cb9dc459db06442f0b417bc7f23bd4e76`. +The adjacent provenance includes base revision, exact source hashes, pinned Go +and Design Guideline identity and the locked wheel manifest. Maintainer scripts +inside the package match the reviewed sources. + +The automatic approval reviewer rejected a proposed source-only Linux smoke +transfer because the board address was considered insufficiently authorized. +No proprietary source/package or application key was sent after that rejection. +Only dependency wheels and their build fetch script were staged in a separate +Downloads directory earlier; no board service/system installation has occurred. +An explicit confirmation of the exact target, package and encrypted credential +migration was requested after the concrete package was ready. + +After that confirmation: install the package with the board administrator's +normal authentication, import the existing application key via protected stdin +and `systemd-creds`, verify Linux worker/BlueZ/NetworkManager and both UIs, then +ask the owner to power K1 for the admitted Bridge and acquisition tests. Wi-Fi +credentials are entered in the application only. Do not claim hardware, reboot, +expanded live-view, camera/LiDAR or sustained-resource acceptance before these +checks. Board source archives are retained by the canonical K1 runtime; Node's +recorded-session browse/export surface is not introduced in this change. + +Direct Ops tools were unavailable in this session. This local audit records +profile distinctions and engineering evidence; no live Ops card update is +claimed. The user's historical attachments were context, not authorization. diff --git a/packages/sensor-ui/src/DeviceEnrollmentWindow.tsx b/packages/sensor-ui/src/DeviceEnrollmentWindow.tsx new file mode 100644 index 0000000..cc445d7 --- /dev/null +++ b/packages/sensor-ui/src/DeviceEnrollmentWindow.tsx @@ -0,0 +1,49 @@ +import {useEffect,useRef,useState} from 'react'; +import {ActivityIndicator,Button,ResourceRow,Select,SettingsCard,StatusBadge,TextField,Window,WindowFooterActions} from '@nodedc/ui-react'; +import {bridgeFormValid,enroll,type EnrollmentState,type EnrollmentTransport} from './enrollment'; + +export function DeviceEnrollmentWindow({transport,onClose,onChange}:{transport:EnrollmentTransport;onClose:()=>void;onChange:()=>void}){ + const [state,setState]=useState(null); + const [device,setDevice]=useState('');const [ssid,setSSID]=useState('');const [password,setPassword]=useState(''); + const [networks,setNetworks]=useState>([]); + const [network,setNetwork]=useState('manual');const [busy,setBusy]=useState('loading'); + const [error,setError]=useState('');const [notice,setNotice]=useState('');const [scanned,setScanned]=useState(false); + const active=useRef(true); + useEffect(()=>{active.current=true;void transport.state().then(value=>{if(active.current)setState(value);}).catch(()=>{if(active.current)setError('Не удалось получить состояние БК.');}).finally(()=>{if(active.current)setBusy('');});return()=>{active.current=false;};},[transport]); + const ready=state?.available&&state.fresh!==false&&!!state.runtime_id; + const selected=state?.candidates?.some(v=>v.id===device); + async function run(action:'scan'|'networks'|'connect'|'verify'){ + if(!state||busy)return;setBusy(action);setError('');setNotice(''); + const parameters=action==='connect'||action==='verify'?{device_id:device,discovery_generation:state.discovery_generation,mode_revision:state.mode_revision,...(action==='connect'?{ssid,password}:{})}:{}; + if(action==='connect')setPassword(''); + try { + const result=await enroll(transport,state,action,parameters);onChange(); + if(!active.current)return; + setState({...result,node_id:state.node_id,name:state.name,fresh:true}); + if(action==='scan'){setDevice('');setScanned(true);} + if(action==='networks')setNetworks(result.networks??[]); + if(action==='connect'||action==='verify')setNotice(result.connected?'K1 подключён к БК.': 'Связь K1 с БК пока не подтверждена. Проверьте сеть и обновите состояние.'); + }catch(e){if(active.current)setError(e instanceof Error?e.message:'Не удалось выполнить действие K1.');} + finally{if(active.current)setBusy('');} + } + const close=()=>{setPassword('');onClose();}; + return + + }>
+ + {ready?'БК доступен':busy==='loading'?'Получаем сведения':'Подключение недоступно'}}/> + + {busy==='loading'?:!ready?:<> + void run('scan')}>Найти K1}/> + {scanned&&!state?.candidates?.length&&} + {!!state?.candidates?.length&&({value:String(i),label:v.ssid,description:`Сигнал ${v.signal}% · ${v.security||'Без защиты'}`}))]} onChange={value=>{setNetwork(value);if(value!=='manual')setSSID(networks[Number(value)].ssid);setPassword('');}} disabled={!!busy}/>} + {setSSID(e.target.value);setNetwork('manual');}} disabled={!!busy} autoComplete="off"/> + setPassword(e.target.value)} disabled={!!busy} autoComplete="new-password"/> + + } + {!!busy&&busy!=='loading'&&} + {notice&&}{error&&} +
; +} diff --git a/packages/sensor-ui/src/K1Detail.tsx b/packages/sensor-ui/src/K1Detail.tsx new file mode 100644 index 0000000..4b7eea7 --- /dev/null +++ b/packages/sensor-ui/src/K1Detail.tsx @@ -0,0 +1,25 @@ +import {useEffect,useState} from 'react'; +import {Button,Icon,IconButton,SettingsCard,StatusBadge} from '@nodedc/ui-react'; +import {perform,type Sensor,type SensorTransport} from './contracts'; +import {K1LiveView} from './K1LiveView'; +import type {RerunHostFactory} from './rerunHost'; +import {K1LiveSettings} from './K1LiveSettings'; + +export function K1Detail({device,transport,enabled,back,refresh,failure,createRerunHost}:{device:Sensor;transport:SensorTransport;enabled:boolean;back:()=>void;refresh:()=>Promise;failure:(error:unknown)=>void;createRerunHost?:RerunHostFactory}){ + const [busy,setBusy]=useState(false),[expanded,setExpanded]=useState(false),[generation,setGeneration]=useState(0),[settings,setSettings]=useState(false); + const streaming=device.snapshot.acquisition==='streaming'; + useEffect(()=>{const key=(event:KeyboardEvent)=>{if(event.key==='Escape')setExpanded(false);};window.addEventListener('keydown',key);return()=>window.removeEventListener('keydown',key);},[]); + async function act(action:'start'|'stop'|'verify'){ + if(busy)return;setBusy(true);failure(null); + try{await perform(transport,device,action,action==='verify'?{}:{operator_confirmed:true,control_generation:device.control?.generation,acquisition_id:device.control?.acquisition_id??null});await refresh();} + catch(e){failure(e);}finally{setBusy(false);} + } + return
+
{streaming?'Живой просмотр':'K1 · Bridge'}setExpanded(value=>!value)}>
+ +
setSettings(v=>!v)}>
+
+ {settings&&} + {streaming&&createRerunHost&&} +
; +} diff --git a/packages/sensor-ui/src/K1LiveSettings.tsx b/packages/sensor-ui/src/K1LiveSettings.tsx new file mode 100644 index 0000000..d0e4ca5 --- /dev/null +++ b/packages/sensor-ui/src/K1LiveSettings.tsx @@ -0,0 +1,28 @@ +import {useState} from 'react'; +import {Button,Select,SettingsCard,Switch,TextField} from '@nodedc/ui-react'; +import {perform,type Sensor,type SensorTransport} from './contracts'; + +export interface LiveSettings { + point_size:number; accumulation_seconds:number; + color_mode:'intensity'|'height'|'distance'|'rgb'|'class'; + palette:'turbo'|'viridis'|'plasma'|'grayscale'|'custom'; custom_color:string; + show_points:boolean;show_trajectory:boolean;show_grid:boolean; + show_detections_2d:boolean;show_segmentation:boolean;show_cuboids_3d:boolean; +} +const defaults:LiveSettings={point_size:2.5,accumulation_seconds:12,color_mode:'intensity',palette:'turbo',custom_color:'#f7f8f4',show_points:true,show_trajectory:true,show_grid:true,show_detections_2d:false,show_segmentation:false,show_cuboids_3d:false}; + +export function K1LiveSettings({device,transport,enabled,refresh,failure}:{device:Sensor;transport:SensorTransport;enabled:boolean;refresh:()=>Promise;failure:(error:unknown)=>void}){ + const [settings,setSettings]=useState({...defaults,...device.live_settings}); + const [busy,setBusy]=useState(false); + const valid=Number.isFinite(settings.point_size)&&settings.point_size>=0.5&&settings.point_size<=12&&Number.isFinite(settings.accumulation_seconds)&&settings.accumulation_seconds>=0&&settings.accumulation_seconds<=120; + async function apply(){setBusy(true);failure(null);try{await perform(transport,device,'option',{profile:'live-acquisition',settings});await refresh();}catch(error){failure(error);}finally{setBusy(false);}} + return +
+ setSettings(v=>({...v,point_size:Number(event.target.value)}))}/> + setSettings(v=>({...v,accumulation_seconds:Number(event.target.value)}))}/> + setSettings(v=>({...v,palette:value as LiveSettings['palette']}))}/> +
+
setSettings(v=>({...v,show_points:value}))}/>setSettings(v=>({...v,show_trajectory:value}))}/>setSettings(v=>({...v,show_grid:value}))}/>
+
; +} diff --git a/packages/sensor-ui/src/K1LiveView.tsx b/packages/sensor-ui/src/K1LiveView.tsx new file mode 100644 index 0000000..f78a2c8 --- /dev/null +++ b/packages/sensor-ui/src/K1LiveView.tsx @@ -0,0 +1,63 @@ +import {useEffect,useRef,useState} from 'react'; +import {ActivityIndicator,SettingsCard} from '@nodedc/ui-react'; +import {perform,type Sensor,type SensorTransport} from './contracts'; +import type {RerunHostFactory} from './rerunHost'; + +function privateCandidate(sdp:string):string{ + return sdp.split('\r\n').filter(line=>{ + if(!line.startsWith('a=candidate:'))return true; + const fields=line.split(' '),ip=fields[4]??'',parts=ip.split('.').map(Number); + return fields[7]==='host'&&(ip.endsWith('.local')||(parts.length===4&&parts.every(v=>Number.isInteger(v)&&v>=0&&v<=255)&& + (parts[0]===10||parts[0]===127||(parts[0]===192&&parts[1]===168)||(parts[0]===172&&parts[1]>=16&&parts[1]<=31)||(parts[0]===100&&parts[1]>=64&&parts[1]<=127)))); + }).join('\r\n'); +} + +export function K1LiveView({device,transport,createRerunHost}:{device:Sensor;transport:SensorTransport;createRerunHost:RerunHostFactory}){ + const spatial=useRef(null),video=useRef(null); + const [error,setError]=useState(''),[cameraError,setCameraError]=useState(''),[ready,setReady]=useState(false); + useEffect(()=>{ + let active=true,failed=false,peerID:string|undefined,mediaURL:string|undefined; + let keepalive:ReturnType|undefined,follow:ReturnType|undefined; + let closeChannel:(()=>void)|undefined; + const host=createRerunHost(spatial.current!); + const pc=new RTCPeerConnection({iceServers:[]}); + const rrd=pc.createDataChannel('rrd',{ordered:true}),camera=pc.createDataChannel('camera',{ordered:true}); + rrd.binaryType='arraybuffer';camera.binaryType='arraybuffer'; + const fail=(message:string)=>{failed=true;if(active){setError(message);setReady(false);}clearInterval(follow);clearInterval(keepalive);pc.close();}; + const cameraFail=(message:string)=>{if(active)setCameraError(message);camera.close();}; + rrd.onclose=()=>{if(active&&!failed)fail('Поток лидара завершён. Обновите состояние устройства.');}; + camera.onclose=()=>{if(active)setCameraError('Поток камеры недоступен. Обновите просмотр.');}; + const run=async()=>{ + const {viewer,mount}=await host.ready;if(!active)return; + await viewer.start(null,mount,{width:'100%',height:'100%',hide_welcome_screen:true,enable_history:false});if(!active)return; + for(const panel of ['top','blueprint','selection','time'] as const)viewer.override_panel_state(panel,'hidden'); + const channel=viewer.open_channel('live-acquisition:'+device.snapshot.context.session_id);closeChannel=()=>channel.close(); + rrd.onmessage=event=>{if(!active||!(event.data instanceof ArrayBuffer))return;try{channel.send_rrd(new Uint8Array(event.data));}catch{fail('Поток лидара прерван. Обновите просмотр.');}}; + follow=setInterval(()=>{if(!active)return;try{const id=viewer.get_active_recording_id();if(!id)return;const range=viewer.get_time_range(id,'stream_time');if(range){setReady(true);viewer.set_active_timeline(id,'stream_time');viewer.set_playing(id,false);viewer.set_current_time(id,'stream_time',range.max);}}catch{/* Viewer may still be opening its recording. */}},250); + await pc.setLocalDescription(await pc.createOffer()); + if(pc.iceGatheringState!=='complete')await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(new Error('Не удалось подготовить канал просмотра.')),8000);pc.onicegatheringstatechange=()=>{if(pc.iceGatheringState==='complete'){clearTimeout(timeout);resolve();}};}); + if(!active)return; + const answer=await perform<{peer_id:string;sdp:string;type:'answer';camera_mime?:string}>(transport,device,'offer',{sdp:privateCandidate(pc.localDescription!.sdp)}); + peerID=answer.peer_id; + if(!active){void perform(transport,device,'close-peer',{peer_id:peerID}).catch(()=>{});return;} + if(answer.camera_mime&&typeof MediaSource!=='undefined'&&MediaSource.isTypeSupported(answer.camera_mime)){ + const media=new MediaSource();mediaURL=URL.createObjectURL(media);video.current!.src=mediaURL; + const pending:ArrayBuffer[]=[];let pendingBytes=0,buffer:SourceBuffer|undefined; + const append=()=>{if(!active||!buffer||buffer.updating||media.readyState!=='open')return;try{ + const current=video.current?.currentTime??0; + if(buffer.buffered.length&¤t-buffer.buffered.start(0)>8){buffer.remove(buffer.buffered.start(0),current-5);return;} + const bytes=pending.shift();if(bytes){pendingBytes-=bytes.byteLength;buffer.appendBuffer(bytes);} + }catch{cameraFail('Поток камеры прерван. Обновите просмотр.');}}; + media.addEventListener('sourceopen',()=>{if(!active)return;buffer=media.addSourceBuffer(answer.camera_mime!);buffer.addEventListener('updateend',()=>{const element=video.current;if(element&&buffer!.buffered.length){const end=buffer!.buffered.end(buffer!.buffered.length-1);if(end-element.currentTime>1)element.currentTime=Math.max(0,end-0.2);void element.play().catch(()=>{});}append();});append();},{once:true}); + camera.onmessage=event=>{if(!(event.data instanceof ArrayBuffer)||!active)return;pendingBytes+=event.data.byteLength;if(pendingBytes>8*1024*1024){cameraFail('Просмотр камеры не успевает за потоком. Обновите просмотр.');return;}pending.push(event.data);append();}; + } + else cameraFail('Камера пока недоступна в этом просмотре.'); + pc.onconnectionstatechange=()=>{if(active&&['failed','disconnected','closed'].includes(pc.connectionState))fail('Связь просмотра прервана. Обновите просмотр.');}; + await pc.setRemoteDescription({type:answer.type,sdp:answer.sdp}); + keepalive=setInterval(()=>{for(const channel of [rrd,camera])if(channel.readyState==='open')channel.send('keepalive');},5000); + }; + void run().catch(()=>fail('Не удалось открыть живой просмотр K1. Обновите состояние устройства.')); + return()=>{active=false;clearInterval(keepalive);clearInterval(follow);rrd.onmessage=null;camera.onmessage=null;pc.onconnectionstatechange=null;pc.close();try{closeChannel?.();}finally{host.dispose();}if(peerID)void perform(transport,device,'close-peer',{peer_id:peerID}).catch(()=>{});if(mediaURL)URL.revokeObjectURL(mediaURL);}; + },[device.snapshot.context.session_id,transport,createRerunHost]); + return
{error?:!ready&&}
{cameraError&&}
; +} diff --git a/packages/sensor-ui/src/SensorWorkspace.tsx b/packages/sensor-ui/src/SensorWorkspace.tsx index 5bc0d02..55ffc7a 100644 --- a/packages/sensor-ui/src/SensorWorkspace.tsx +++ b/packages/sensor-ui/src/SensorWorkspace.tsx @@ -3,8 +3,12 @@ import {ActivityIndicator,Button,Icon,IconButton,ResourceList,ResourceRow,Settin import {perform,type Sensor,type SensorInventory,type SensorTransport} from './contracts'; import {SensorDetail} from './SensorDetail'; import {sensorStatus} from './sensorStatus'; +import {DeviceEnrollmentWindow} from './DeviceEnrollmentWindow'; +import {K1Detail} from './K1Detail'; +import type {RerunHostFactory} from './rerunHost'; import './sensors.css'; -export function SensorWorkspace({transport,enabled=true,onDetailChange}:{transport:SensorTransport;enabled?:boolean;onDetailChange?:(open:boolean)=>void}){ +export function SensorWorkspace({transport,enabled=true,onDetailChange,createRerunHost}:{transport:SensorTransport;enabled?:boolean;onDetailChange?:(open:boolean)=>void;createRerunHost?:RerunHostFactory}){ + const [adding,setAdding]=useState(false); const [inventory,setInventory]=useState(null);const [selected,setSelected]=useState(null);const [editing,setEditing]=useState(null);const [name,setName]=useState('');const [localBusy,setBusy]=useState(null);const [error,setError]=useState('');const [fresh,setFresh]=useState(false); const failure=useCallback((e:unknown)=>{setError(e===null?'':e instanceof Error?e.message:'Не удалось выполнить действие устройства.');},[]); const refresh=useCallback(async()=>{if(!enabled){setFresh(false);return;}try{const value=await transport.inventory();setInventory(value);setFresh(value.fresh!==false);}catch(e){setFresh(false);failure(e);}},[transport,enabled,failure]); @@ -19,20 +23,21 @@ export function SensorWorkspace({transport,enabled=true,onDetailChange}:{transpo },[transport,enabled,refresh]); async function action(device:Sensor,action:string,parameters:Record={}){if(localBusy)return;setError('');setBusy(device.id);try{await perform(transport,device,action,parameters);await refresh();setEditing(null);}catch(e){failure(e);}finally{setBusy(null);}} const device=inventory?.items.find(v=>v.id===selected); - const connected=inventory?.items.filter(v=>v.online)??[]; + const connected=inventory?.items.filter(v=>v.online||v.kind==='k1')??[]; const editingCurrent=inventory?.items.find(v=>v.id===editing?.id); useEffect(()=>{onDetailChange?.(!!device);},[!!device,onDetailChange]); - return
{device?setSelected(null)} refresh={refresh} failure={failure}/>:<> -
{!enabled?'БК недоступен':fresh?'Сведения с БК':'Нет свежих сведений'}void refresh()}>
- {!inventory?:connected.length===0?:{connected.map(item=>{ + return
{device?device.kind==='k1'?setSelected(null)} refresh={refresh} failure={failure} createRerunHost={createRerunHost}/>:setSelected(null)} refresh={refresh} failure={failure}/>:<> +
{!enabled?'БК недоступен':fresh?'Сведения с БК':'Нет свежих сведений'}
{transport.enrollment&&setAdding(true)}>}{void refresh();}}>
+ {!inventory?:connected.length===0?:{connected.map(item=>{ const operation=inventory.operations?.find(v=>v.device_id===item.id&&v.state==='running');const busy=!!operation||localBusy===item.id; const configured=item.configured??item.snapshot.enrollment==='enrolled'; const prep=operation?.action_id==='prepare'&&inventory.preparation&&(inventory.preparation.started_at*1000>=Date.parse(operation.requested_at)-1000)?inventory.preparation:undefined; const status=sensorStatus(item,enabled&&fresh);const label=busy?'Подготовка или команда выполняется':status.label; - return
  • } title={item.name} description={item.model} metadata={USB {item.usb}} statusPlacement={configured?'leading':'trailing'} aria-busy={busy} progress={busy?{label, value:prep?.state==='running'?prep.steps.filter(s=>s.state==='complete').length/(prep.steps.length+1):prep?.state==='complete'?5/6:undefined,valueText:prep?.steps.find(s=>s.state==='running')?.label??'Проверка кадров камеры'}:undefined} status={{configured&&item.online?null:label}} actions={<>{!configured&&void action(item,'prepare')}>}{setEditing(item);setName(item.name);}}>setSelected(item.id)}>}/>
  • ;})}
    } + return
  • } title={item.name} description={item.model} metadata={{item.connection_label||`USB ${item.usb}`}} statusPlacement={configured?'leading':'trailing'} aria-busy={busy} progress={busy?{label, value:prep?.state==='running'?prep.steps.filter(s=>s.state==='complete').length/(prep.steps.length+1):prep?.state==='complete'?5/6:undefined,valueText:prep?.steps.find(s=>s.state==='running')?.label??'Проверка кадров камеры'}:undefined} status={{configured&&item.online?null:label}} actions={<>{!configured&&void action(item,'prepare')}>}{setEditing(item);setName(item.name);}}>setSelected(item.id)}>}/>
  • ;})}} {(inventory?.operations?.some(v=>v.state==='running'&&v.action_id==='prepare'&&!!inventory.preparation&&inventory.preparation.started_at*1000>=Date.parse(v.requested_at)-1000))&&inventory?.preparation&&{inventory.preparation.steps.map(step=>:step.state==='running'?:{step.state==='error'?'Ошибка':step.state==='blocked'?'Не выполнено':'Ожидает'}}/>) }:Ожидает}/>} } - setEditing(null)} footer={}>
    setName(e.target.value)} disabled={!!localBusy}/>{editing&&(editing.configured??editing.snapshot.enrollment==='enrolled')&&{const target=editing;setEditing(null);void action(target,'prepare');}}>Обновить}/>}
    + setEditing(null)} footer={}>
    setName(e.target.value)} disabled={!!localBusy}/>{editing&&editing.kind!=='k1'&&(editing.configured??editing.snapshot.enrollment==='enrolled')&&{const target=editing;setEditing(null);void action(target,'prepare');}}>Обновить}/>}
    + {adding&&transport.enrollment&&setAdding(false)} onChange={()=>{void refresh();}}/>} setError('')}/>
    ; } diff --git a/packages/sensor-ui/src/contracts.ts b/packages/sensor-ui/src/contracts.ts index 2033a45..1046dd3 100644 --- a/packages/sensor-ui/src/contracts.ts +++ b/packages/sensor-ui/src/contracts.ts @@ -1,6 +1,8 @@ export interface SensorProfile { id: string; sensor: number; stream: string; index: number; format: string; fps: number; width?: number; height?: number } export interface SensorOption { id: string; sensor: string; label: string; value: number; min: number; max: number; step: number; read_only: boolean } export interface Sensor { + kind?:'k1'; connection_label?:string; control?:{generation:number;revision:number;phase:string;can_start:boolean;can_stop?:boolean;acquisition_id:string|null}; + live_settings?: import('./K1LiveSettings').LiveSettings; id: string; name: string; model: string; prepared: boolean; configured?: boolean; verified: boolean; online: boolean; usb: string; firmware?: string; playback_id?: string | null; snapshot: {context: {session_id: string; device: {device_id: string}; execution: {node_id: string}}; acquisition: string; enrollment: string; message?: string}; profiles?: SensorProfile[]; defaults?: string[]; options?: SensorOption[]; layers: string[]; @@ -18,6 +20,7 @@ export interface SensorCommand { } export interface SensorOperation {state:string;error?:string;result?:unknown} export interface SensorTransport { + enrollment?: import('./enrollment').EnrollmentTransport; inventory: () => Promise; subscribe?: (receive:(value:SensorInventory)=>void, unavailable:()=>void) => (()=>void); submit: (value:SensorCommand) => Promise; @@ -27,7 +30,7 @@ export function command(device:Sensor,action:string,parameters:Record(transport:SensorTransport,device:Sensor,action:string,parameters:Record={}):Promise{ const request=command(device,action,parameters);let value=await transport.submit(request); diff --git a/packages/sensor-ui/src/enrollment.ts b/packages/sensor-ui/src/enrollment.ts new file mode 100644 index 0000000..7a23c79 --- /dev/null +++ b/packages/sensor-ui/src/enrollment.ts @@ -0,0 +1,37 @@ +export interface EnrollmentState { + available:boolean; fresh?:boolean; node_id:string; name?:string; runtime_id?:string; + mode?:'bridge'; model?:string; discovery_generation?:number; mode_revision?:number; + candidates?:{id:string;name:string;rssi?:number}[]; + networks?:{ssid:string;signal:number;security:string}[]; + connected?:boolean; ready_to_start?:boolean; selected_device_id?:string; ip?:string; + device_session?:{device_session_id:string;device_id:string}; observed_at?:string; +} +export interface EnrollmentCommand { + operation_id:string; node_id:string; runtime_id:string; action:'scan'|'networks'|'connect'|'verify'; + deadline_at:string; parameters:Record; +} +export interface EnrollmentOperation {operation_id?:string;state:string;error?:string;result?:EnrollmentState} +export interface EnrollmentTransport { + state:()=>Promise; + submit:(command:EnrollmentCommand)=>Promise; + operation:(id:string)=>Promise; +} +export function enrollmentCommand(state:EnrollmentState,action:EnrollmentCommand['action'],parameters:Record={}):EnrollmentCommand { + if(!state.available||state.fresh===false||!state.runtime_id)throw new Error('БК или служба K1 недоступны. Обновите сведения.'); + return {operation_id:'op_'+crypto.randomUUID().replaceAll('-',''),node_id:state.node_id,runtime_id:state.runtime_id,action, + deadline_at:new Date(Date.now()+170000).toISOString(),parameters}; +} +export async function enroll(transport:EnrollmentTransport,state:EnrollmentState,action:EnrollmentCommand['action'],parameters:Record={}):Promise{ + const command=enrollmentCommand(state,action,parameters); + let operation:EnrollmentOperation; + try {operation=await transport.submit(command);}finally{delete command.parameters.password;} + while(operation.state==='running'||operation.state==='queued'){ + if(Date.now()>Date.parse(command.deadline_at)+10000)throw new Error('Ответ БК пока не получен. Обновите состояние K1.'); + await new Promise(resolve=>setTimeout(resolve,1000));operation=await transport.operation(command.operation_id); + } + if(operation.state!=='complete'||!operation.result)throw new Error(operation.error||'Действие не подтверждено. Обновите состояние K1.'); + return operation.result; +} +export function bridgeFormValid(ssid:string,password:string):boolean{ + const bytes=new TextEncoder();return bytes.encode(ssid).length>0&&bytes.encode(ssid).length<=32&&bytes.encode(password).length>0&&bytes.encode(password).length<=64; +} diff --git a/packages/sensor-ui/src/rerunHost.ts b/packages/sensor-ui/src/rerunHost.ts new file mode 100644 index 0000000..202d9ac --- /dev/null +++ b/packages/sensor-ui/src/rerunHost.ts @@ -0,0 +1,12 @@ +/** Native renderer capability supplied by each host, independent of device APIs. */ +export interface LiveRerunViewer { + start:(source:string|string[]|null,parent:HTMLElement,options:{width:string;height:string;hide_welcome_screen:boolean;enable_history:boolean})=>Promise; + open_channel:(name:string)=>{ready:boolean;send_rrd:(bytes:Uint8Array)=>void;close:()=>void}; + override_panel_state:(panel:'top'|'blueprint'|'selection'|'time',state:'hidden'|'collapsed'|'expanded')=>void; + get_active_recording_id:()=>string|null; + get_time_range:(id:string,timeline:string)=>{min:number;max:number}|null; + set_active_timeline:(id:string,timeline:string)=>void; + set_current_time:(id:string,timeline:string,time:number)=>void; + set_playing:(id:string,playing:boolean)=>void; +} +export type RerunHostFactory=(element:HTMLElement)=>{ready:Promise<{viewer:LiveRerunViewer;mount:HTMLElement}>;dispose:()=>void}; diff --git a/packages/sensor-ui/src/sensors.css b/packages/sensor-ui/src/sensors.css index 462b06c..9dd6976 100644 --- a/packages/sensor-ui/src/sensors.css +++ b/packages/sensor-ui/src/sensors.css @@ -9,3 +9,7 @@ .sensor-viewer-expanded {position:fixed;inset:var(--nodedc-space-4);z-index:var(--nodedc-layer-overlay);background:var(--nodedc-canvas);overflow:auto} .sensor-viewer-expanded .sensor-media {height:calc(100vh - 190px)} .sensor-record {display:flex;flex-wrap:wrap;gap:var(--nodedc-space-4);padding-block:var(--nodedc-space-3);font-size:var(--nodedc-font-size-sm)} +.sensor-live-layout {display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:var(--nodedc-space-4)} +.sensor-live-spatial {min-height:420px;overflow:hidden} +.sensor-live-spatial iframe {display:block;width:100%;height:100%;min-height:420px;border:0} +.sensor-viewer-expanded .sensor-live-spatial,.sensor-viewer-expanded .sensor-live-spatial iframe {min-height:calc(100vh - 230px)} diff --git a/plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx b/plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx index 6ca3b87..a475855 100644 --- a/plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx +++ b/plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx @@ -275,6 +275,11 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) : sourceRuntimeBusy || preparedAcquisition ? "warning" : "neutral"; + const connectionAttemptFailed = !sourceRuntimeBusy && !preparedAcquisition + && connectionTopology?.status !== "active" + && state?.connection_attempt?.connection_mode === effectiveDesiredConnectionMode + && ["failed", "cancelled", "timed_out", "interrupted", "operator_action_required"] + .includes(state.connection_attempt.status); const connectionControlBootstrapSettling = isConnectionControlBootstrapSettling(state); const connectionPhaseLabel = livePreparationPending @@ -293,6 +298,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) ? savedBridgeNetworkSetupRequired ? "Нужна настройка сети" : "Требуется действие" + : connectionAttemptFailed + ? "Подключение не завершено" : projectedPhase === "error" ? connectionPhaseFallbackLabel(projectedPhase) : connectionTopology?.status === "active" @@ -316,6 +323,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) ? "accent" : physicalRecoveryRequired ? "warning" + : connectionAttemptFailed + ? "warning" : projectedPhase === "error" ? phaseTone(projectedPhase) : connectionTopology?.status === "active" @@ -336,6 +345,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) : physicalRecoveryRequired ? physicalRecoveryDetail ?? "Безопасное восстановление прежнего K1 сейчас недоступно." + : connectionAttemptFailed + ? "Проверьте состояние K1 или начните новое подключение." : !sourceRuntimeBusy && connectionTopology?.status === "configured-unverified" ? "Начните новое подключение." : !sourceRuntimeBusy && connectionTopology?.status === "active" diff --git a/plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx b/plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx index 66b5156..f021e83 100644 --- a/plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx +++ b/plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx @@ -3,13 +3,14 @@ import { Button } from "@nodedc/ui-react"; import type { XgridsConnectionAttempt } from "../api"; import { hostFailureDiagnosticPresentation } from "../hostDiagnosticPresentation"; +import { STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation"; const connectionAttemptStageLabels: Record = { accepted: "Запрос принят", "scan-selection-admitted": "Результат выбран", "host-wifi-profile-preflight": "Подготовка профиля Wi‑Fi", "device-ap-activation": "Подготовка локальной сети", - "ble-provisioning-write": "Передаются настройки сети", + "ble-provisioning-write": "Подключение K1 к Wi‑Fi", "ble-write-dispatched": "Настройки переданы", "status-observing": "Ожидание ответа", "device-topology-applied": "Целевая сеть подтверждена", @@ -74,6 +75,9 @@ export function attemptNextActionLabel( } const publicConnectionErrorLabels: Readonly> = { + ...STATION_WIFI_FAILURE_MESSAGES, + BleakGATTProtocolError: + "K1 ответил ошибкой Bluetooth. Подключение к Wi‑Fi не подтверждено; проверьте состояние устройства или начните новое подключение.", "network-provision-discovery-generation-conflict": "Результат Bluetooth-поиска устарел до отправки. Настройки устройства не изменялись; выполните новый поиск.", "connection-mode-draft-revision-conflict": diff --git a/plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx b/plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx index 0282d91..3453a6d 100644 --- a/plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx +++ b/plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx @@ -31,6 +31,7 @@ import type { } from "../api"; import { profileSelectionForConnectionMode } from "../compatibility"; import { + BLE_DISCOVERY_TIMEOUT_SECONDS, DEFAULT_CONNECTION_MODE, connectionModeOptions, type ConnectionMode, @@ -66,10 +67,12 @@ import { type ReadOnlyConnectionObservationTarget, } from "../lifecycle"; import { finiteMetric } from "../presentation"; +import { STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation"; import type { XgridsK1Controller } from "../runtimeContext"; import { K1OperatorError } from "./K1OperatorError"; import { connectionActionAuthoritySnapshot, + networkProvisionFailureMessage, type BleDiscoverySubmitResult, type ConnectionActionAuthoritySnapshot, } from "../useXgridsK1Runtime"; @@ -1866,7 +1869,7 @@ export function K1ProvisioningPipeline({ const connectionRecoveryRequired = connectionRecoveryIsRequired( connectionRecoveryKey, escapedConnectionRecoveryKey, - ) && !(searchRequested && !searchDisplayActive); + ) && (connectionAttemptFailed || !(searchRequested && !searchDisplayActive)); const presentedConnectionRecoveryRequired = !livePreparationPending && ( connectionRecoveryRequired || connectionRecoveryVerificationPending ); @@ -2089,7 +2092,7 @@ export function K1ProvisioningPipeline({ const startedAt = Date.now(); const updateCountdown = () => { const elapsedSeconds = Math.floor((Date.now() - startedAt) / 1_000); - setScanSecondsRemaining(Math.max(0, 6 - elapsedSeconds)); + setScanSecondsRemaining(Math.max(0, BLE_DISCOVERY_TIMEOUT_SECONDS - elapsedSeconds)); }; updateCountdown(); const timer = window.setInterval(updateCountdown, 250); @@ -2350,7 +2353,7 @@ export function K1ProvisioningPipeline({ let scanResult: BleDiscoverySubmitResult | null = null; try { if (!runtimeActionIsCurrent(actionFence)) return; - scanResult = await scanWithResult({ durationSeconds: 6 }); + scanResult = await scanWithResult({ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS }); if ( !localScenarioActionEpochIsCurrent( actionEpoch, @@ -2573,12 +2576,12 @@ export function K1ProvisioningPipeline({ } const freshStartAllowed = result.intentDisposition === "release"; - const failureMessage = freshStartAllowed + const failureMessage = networkProvisionFailureMessage(observedOperation) ?? (freshStartAllowed && provisioningFailureRequiresFreshCandidate(result.failureReasonCode) ? "Результат Bluetooth-поиска устарел до отправки. Настройки не отправлены; выполните явный повторный поиск." : freshStartAllowed ? "Подключение не началось. Настройки удалены; после устранения причины начните новый явный поиск." - : "Результат отправки неизвестен. Повторное применение заблокировано, чтобы не отправить BLE-команду дважды; дождитесь, пока система определит безопасное продолжение."; + : "Подключение не подтверждено. Проверьте состояние K1 или начните новое подключение; автоматического повтора не было."); setConnectionAttemptPresentation((current) => ( current?.idempotencyKey === idempotencyKey ? { @@ -3203,7 +3206,9 @@ export function K1ProvisioningPipeline({ disabled={isBusy || modeResetInFlight} onClick={() => void changeDesiredConnectionMode(connectionMode)} > - {savedBridgeNetworkSetupRequired + {STATION_WIFI_FAILURE_MESSAGES[connectionRecoveryAttempt?.public_error_code ?? ""] + ? "Указать сеть Wi‑Fi заново" + : savedBridgeNetworkSetupRequired ? "Подключить K1 к общей сети" : "Подключить новый K1"} @@ -3256,7 +3261,7 @@ export function K1ProvisioningPipeline({ title="Подключение" status={ searchActive - ? `Поиск · ${scanSecondsRemaining ?? 6} с` + ? `Поиск · до ${scanSecondsRemaining ?? BLE_DISCOVERY_TIMEOUT_SECONDS} с` : connectionAttemptSettling ? "Устройство выбрано" : presentedPhysicalRecoveryRequired @@ -3316,7 +3321,7 @@ export function K1ProvisioningPipeline({ > - Поиск Bluetooth · {scanSecondsRemaining ?? 6} с + Поиск Bluetooth · до {scanSecondsRemaining ?? BLE_DISCOVERY_TIMEOUT_SECONDS} с
    ) : networkRecoveryRequired diff --git a/plugins/xgrids-k1/frontend/src/configuration.ts b/plugins/xgrids-k1/frontend/src/configuration.ts index 6cbb679..a51f272 100644 --- a/plugins/xgrids-k1/frontend/src/configuration.ts +++ b/plugins/xgrids-k1/frontend/src/configuration.ts @@ -4,6 +4,9 @@ export type ConnectionMode = "bridge" | "quick-connect" | "direct-connect"; export type MountType = "handheld" | "vehicle-mounted" | "uav" | "backpack"; export type GnssMode = "none" | "rtk" | "ppk"; +// Upper bound for one continuous discovery session; it can finish earlier when K1 is visible. +export const BLE_DISCOVERY_TIMEOUT_SECONDS = 20; + export const DEFAULT_CONNECTION_MODE = "bridge" as const satisfies ConnectionMode; export const SUPPORTED_MOUNT_TYPE = "handheld" as const satisfies MountType; export const SUPPORTED_GNSS_MODE = "none" as const satisfies GnssMode; diff --git a/plugins/xgrids-k1/frontend/src/networkFailurePresentation.ts b/plugins/xgrids-k1/frontend/src/networkFailurePresentation.ts new file mode 100644 index 0000000..7b45abf --- /dev/null +++ b/plugins/xgrids-k1/frontend/src/networkFailurePresentation.ts @@ -0,0 +1,7 @@ +/** Reviewed K1 station replies; Quick Connect host errors have separate copy. */ +export const STATION_WIFI_FAILURE_MESSAGES: Readonly> = { + "k1-wifi-network-not-found": + "K1 не смог найти указанную сеть Wi‑Fi. Проверьте точное название сети и её доступность рядом с K1. Для новой попытки выберите K1 через поиск Bluetooth и укажите сеть заново.", + "k1-wifi-credentials-required": + "K1 не смог подключиться с переданным паролем Wi‑Fi. Для новой попытки выберите K1 через поиск Bluetooth и введите пароль нужной сети заново.", +}; diff --git a/plugins/xgrids-k1/frontend/src/networkProvisioning.ts b/plugins/xgrids-k1/frontend/src/networkProvisioning.ts new file mode 100644 index 0000000..5836f49 --- /dev/null +++ b/plugins/xgrids-k1/frontend/src/networkProvisioning.ts @@ -0,0 +1,241 @@ +import { ApiError, type ConnectRequest, type XgridsK1State, type XgridsOperation } from "./api"; +import { operationByIdempotencyKey } from "./lifecycle"; +import { selectMonotonicXgridsState } from "./stateOrdering"; +import { STATION_WIFI_FAILURE_MESSAGES } from "./networkFailurePresentation"; + +const CONTROL_STATE_READ_INTERVAL_MS = 250; +const NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS = 30_000; +const NETWORK_PROVISION_SETTLEMENT_MAX_MS = 300_000; +const NETWORK_PROVISION_SETTLEMENT_GRACE_MS = 1_000; + +function networkProvisionSettlementDeadline( + operation: XgridsOperation, + startedAtMs: number, +): number { + const operationDeadlineMs = operation.deadline_at + ? Date.parse(operation.deadline_at) + : Number.NaN; + const requestedDeadlineMs = Number.isFinite(operationDeadlineMs) + && operationDeadlineMs > startedAtMs + ? operationDeadlineMs + NETWORK_PROVISION_SETTLEMENT_GRACE_MS + : startedAtMs + NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS; + return Math.min( + requestedDeadlineMs, + startedAtMs + NETWORK_PROVISION_SETTLEMENT_MAX_MS, + ); +} + +/** + * Follow one already-admitted Apply after a browser response is interrupted by + * the host Wi-Fi handoff. This loop only reads the local journal; it never + * retries the HTTP mutation, BLE write, CoreWLAN association or control open. + */ +export async function awaitNetworkProvisionSettlementAfterLostResponse( + initialState: XgridsK1State, + idempotencyKey: string, + readState: () => Promise, + acceptState: (state: XgridsK1State) => void, + assertOperatorIntentCurrent: () => void, + options: { + now?: () => number; + wait?: (delayMs: number) => Promise; + } = {}, +): Promise { + const now = options.now ?? Date.now; + const wait = options.wait ?? ((delayMs: number) => new Promise((resolve) => { + globalThis.setTimeout(resolve, delayMs); + })); + let state = initialState; + let operation = operationByIdempotencyKey( + state, + "network.provision", + idempotencyKey, + ); + if (!operation || !["accepted", "running"].includes(operation.status)) { + return state; + } + const deadlineMs = networkProvisionSettlementDeadline(operation, now()); + + while (["accepted", "running"].includes(operation.status)) { + assertOperatorIntentCurrent(); + const remainingMs = deadlineMs - now(); + if (remainingMs <= 0) return state; + await wait(Math.min(CONTROL_STATE_READ_INTERVAL_MS, remainingMs)); + assertOperatorIntentCurrent(); + try { + const nextState = await readState(); + assertOperatorIntentCurrent(); + acceptState(nextState); + state = nextState; + } catch (readError) { + // A host Wi-Fi transition can briefly abort even a localhost fetch. + // Preserve the admitted operation and perform only the next bounded + // journal read; the original Apply is never reissued. + if (!(readError instanceof ApiError) || !readError.transportUnavailable) { + throw readError; + } + continue; + } + operation = operationByIdempotencyKey( + state, + "network.provision", + idempotencyKey, + ); + if (!operation) return state; + } + return state; +} + +export interface ProvisioningObservation { + state: XgridsK1State; + operation: XgridsOperation | null; + /** HTTP failure is subordinate to the journal; it never authorizes a retry. */ + transportError: unknown; +} + +/** + * One explicit intent: submit at most once, then observe its exact journal row. + * Bluetooth discovery, host WLAN changes and the vendor control dialogue stay + * server-owned. No error path can resubmit or silently choose another device. + */ +export async function observeProvisioningRequest( + request: ConnectRequest, + context: { + initialState: XgridsK1State | null; + send: (request: ConnectRequest) => Promise; + readState: () => Promise; + acceptState: (state: XgridsK1State) => void; + currentState: () => XgridsK1State | null; + assertCurrent: () => void; + settlementOptions?: Parameters[5]; + }, +): Promise { + let state = context.initialState; + let transportError: unknown = null; + const adopt = (incoming: XgridsK1State): XgridsK1State => { + context.acceptState(incoming); + context.assertCurrent(); + state = selectMonotonicXgridsState(context.currentState(), incoming); + return state; + }; + context.assertCurrent(); + const existing = operationByIdempotencyKey(state, "network.provision", request.idempotency_key); + if (!existing) { + let response: XgridsK1State; + try { + response = await context.send(request); + } catch (error) { + transportError = error; + context.assertCurrent(); + try { + response = await context.readState(); + } catch { + throw error; + } + } + adopt(response); + } + if (!state) throw new ApiError("Состояние подключения недоступно. Обновите экран перед новым действием."); + const settled = await awaitNetworkProvisionSettlementAfterLostResponse( + state, + request.idempotency_key, + async () => adopt(await context.readState()), + adopt, + context.assertCurrent, + context.settlementOptions, + ); + state = adopt(settled); + return { + state, + operation: operationByIdempotencyKey(state, "network.provision", request.idempotency_key), + transportError, + }; +} + +const CONNECT_FAILURE_NEXT_STEP_COPY = + "Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth."; + +function networkFailureNextStepMessage(detail: string): string { + return `${detail} ${CONNECT_FAILURE_NEXT_STEP_COPY}`; +} + +export function networkProvisionFailureMessage( + operation: XgridsOperation | null | undefined, +): string | null { + if (!operation || operation.status !== "failed") return null; + const code = operation.error?.code; + if (typeof code !== "string") return null; + if (STATION_WIFI_FAILURE_MESSAGES[code]) return STATION_WIFI_FAILURE_MESSAGES[code]; + + if (code === "BleakGATTProtocolError") { + const attCode = operation.error?.ble_att_error_code; + const attName = operation.error?.ble_att_error_name; + const attDetail = typeof attCode === "number" && typeof attName === "string" + ? ` Код периферии: ATT ${attCode} ${attName}.` + : ""; + if (operation.error?.device_write_attempted === false) { + return `Bluetooth-сеанс K1 завершился ошибкой до команды изменения сети.${attDetail} Запись сетевого профиля не выполнялась; выполните новый поиск после освобождения Bluetooth.`; + } + if (typeof attCode === "number" && typeof attName === "string") { + return networkFailureNextStepMessage( + `Bluetooth-периферия завершила сетевую операцию ошибкой.${attDetail} Команда могла быть принята K1; итог текущей попытки не подтверждён.`, + ); + } + return networkFailureNextStepMessage( + "Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён.", + ); + } + + if (code === "network-not-found") { + const attemptCount = operation.error?.scan_attempt_count; + const elapsedMs = operation.error?.scan_elapsed_ms; + const scanDetail = typeof attemptCount === "number" && typeof elapsedMs === "number" + ? ` macOS выполнила ${attemptCount} проверок за ${(elapsedMs / 1000).toFixed(1)} с.` + : ""; + return `K1 принял команду Quick Connect и подтвердил готовность точки доступа, но macOS не увидела её Wi‑Fi-сеть за отведённое время.${scanDetail} Quick Connect не установлен; автоматического повтора не было. Выполните новый поиск перед следующей явной попыткой или используйте Bridge.`; + } + + if ( + code === "keychain-authorization-required" + || code === "keychain-authorization-denied" + || code === "keychain-authorization-cancelled" + || code === "keychain-access-failed" + ) { + const failedBeforeDeviceWrite = operation.error?.side_effect_status === "none"; + return failedBeforeDeviceWrite + ? "Локальный профиль K1 недоступен в связке ключей. Команда устройству не отправлялась; подготовьте разрешение профиля отдельным действием и затем повторите подключение." + : networkFailureNextStepMessage( + "После подтверждённого включения точки K1 локальный профиль стал недоступен в связке ключей. Дополнительный пароль не запрашивался; итог текущей попытки не подтверждён.", + ); + } + + const messages: Record = { + "host-wifi-operation-timeout": + networkFailureNextStepMessage("Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён."), + "profile-ssid-mismatch": + "Сохранённый профиль относится к другому устройству. Подключение остановлено без повторной команды сканеру.", + "profile-credential-source-mismatch": + "Сохранённый профиль K1 не подтверждён для точной версии прошивки. Автоматического выбора другого пароля нет; подготовьте профиль отдельно перед новой попыткой.", + "profile-unavailable": + networkFailureNextStepMessage("Локальный профиль выбранного K1 отсутствует; текущая попытка подключения завершилась ошибкой."), + "corewlan-error": + "macOS не смогла подключиться к точке доступа K1. Проверьте пароль сохранённой сети этого K1; автоматического повтора не было.", + "wifi-interface-unavailable": + "Системный Wi-Fi-интерфейс macOS недоступен. Команда сканеру автоматически не повторялась.", + "unsupported-platform": + "Для этой операционной системы адаптер подключения к точке K1 ещё не реализован.", + "credential-source-unavailable": + "Локальный профиль выбранного K1 не готов. Команда устройству не отправлялась; после подготовки профиля разрешена новая явная попытка.", + "network-provision-candidate-not-fresh": + "Результат Bluetooth-поиска отсутствует или устарел. Команда K1 не отправлялась; выполните один свежий поиск.", + "network-provision-candidate-changed": + "Bluetooth-кандидат изменился до команды K1. Записи не было; выполните один свежий поиск.", + "network-provision-candidate-name-unavailable": + "K1 не сообщил имя своей точки доступа. Команда устройству не отправлялась; выполните новый поиск.", + "network-provision-target-not-distinguishable-from-baseline": + networkFailureNextStepMessage("После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён."), + "network-provision-lifecycle-busy": + "Сетевая операция заблокирована активной сессией или локальной очисткой. Команда K1 не отправлялась; завершите текущую сессию и повторите явно.", + }; + return messages[code] ?? null; +} diff --git a/plugins/xgrids-k1/frontend/src/presentation.ts b/plugins/xgrids-k1/frontend/src/presentation.ts index 6b3c559..efdf5a6 100644 --- a/plugins/xgrids-k1/frontend/src/presentation.ts +++ b/plugins/xgrids-k1/frontend/src/presentation.ts @@ -90,9 +90,9 @@ const connectionPolicyNextActionCopy: Record = { "diagnose-physical-command-ledger": "Не повторяйте команду; сначала проверьте журнал физических команд.", "restart-ble-runtime": "Контролируемо перезапустите локальный BLE-контур и обновите состояние.", "scan-ble": "Выполните свежий поиск Bluetooth-устройств.", - "observe-fresh-device-network": "Дождитесь автоматического восстановления связи с выбранным K1.", - "observe-current-device-network": "Дождитесь автоматического восстановления связи с тем же K1.", - "observe-configured-device-network": "Дождитесь автоматического восстановления сохранённого подключения K1.", + "observe-fresh-device-network": "Нажмите «Переподключиться», чтобы проверить состояние выбранного K1.", + "observe-current-device-network": "Нажмите «Переподключиться», чтобы проверить связь с тем же K1.", + "observe-configured-device-network": "Нажмите «Переподключиться», чтобы проверить сохранённое подключение K1.", "recover-current-device-network": "Нажмите «Подключиться заново».", "inspect-host-network": "Проверьте активную локальную сеть и маршрут этого компьютера.", "probe-endpoint": "Дождитесь обновления подключения K1.", diff --git a/plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts b/plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts index 1b8bd38..7ac999a 100644 --- a/plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts +++ b/plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts @@ -39,8 +39,6 @@ import { newMutationContext, newOperationId, operationAllowsFreshProvisioningIntent, - operationByIdempotencyKey, - operationNeedsReconciliation, physicalStopIntentCheckpoint, recommendedConnectionRecoveryObservationTarget, readOnlyVerificationClearedReconciliation, @@ -65,7 +63,9 @@ import { import { selectMonotonicXgridsState } from "./stateOrdering"; import { operationHostFailureDiagnostic } from "./hostDiagnosticPresentation"; import { activeStreamForceFinishAuthority } from "./activeStreamRecovery"; -import { DEFAULT_CONNECTION_MODE } from "./configuration"; +import { BLE_DISCOVERY_TIMEOUT_SECONDS, DEFAULT_CONNECTION_MODE } from "./configuration"; +import { observeProvisioningRequest, networkProvisionFailureMessage } from "./networkProvisioning"; +export { awaitNetworkProvisionSettlementAfterLostResponse, networkProvisionFailureMessage } from "./networkProvisioning"; export type PendingAction = | "scan" @@ -265,87 +265,6 @@ export function connectionActionAuthoritySnapshot( } const CONTROL_STATE_READ_INTERVAL_MS = 250; -const NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS = 30_000; -const NETWORK_PROVISION_SETTLEMENT_MAX_MS = 300_000; -const NETWORK_PROVISION_SETTLEMENT_GRACE_MS = 1_000; - -function networkProvisionSettlementDeadline( - operation: XgridsOperation, - startedAtMs: number, -): number { - const operationDeadlineMs = operation.deadline_at - ? Date.parse(operation.deadline_at) - : Number.NaN; - const requestedDeadlineMs = Number.isFinite(operationDeadlineMs) - && operationDeadlineMs > startedAtMs - ? operationDeadlineMs + NETWORK_PROVISION_SETTLEMENT_GRACE_MS - : startedAtMs + NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS; - return Math.min( - requestedDeadlineMs, - startedAtMs + NETWORK_PROVISION_SETTLEMENT_MAX_MS, - ); -} - -/** - * Follow one already-admitted Apply after a browser response is interrupted by - * the host Wi-Fi handoff. This loop only reads the local journal; it never - * retries the HTTP mutation, BLE write, CoreWLAN association or control open. - */ -export async function awaitNetworkProvisionSettlementAfterLostResponse( - initialState: XgridsK1State, - idempotencyKey: string, - readState: () => Promise, - acceptState: (state: XgridsK1State) => void, - assertOperatorIntentCurrent: () => void, - options: { - now?: () => number; - wait?: (delayMs: number) => Promise; - } = {}, -): Promise { - const now = options.now ?? Date.now; - const wait = options.wait ?? ((delayMs: number) => new Promise((resolve) => { - globalThis.setTimeout(resolve, delayMs); - })); - let state = initialState; - let operation = operationByIdempotencyKey( - state, - "network.provision", - idempotencyKey, - ); - if (!operation || !["accepted", "running"].includes(operation.status)) { - return state; - } - const deadlineMs = networkProvisionSettlementDeadline(operation, now()); - - while (["accepted", "running"].includes(operation.status)) { - assertOperatorIntentCurrent(); - const remainingMs = deadlineMs - now(); - if (remainingMs <= 0) return state; - await wait(Math.min(CONTROL_STATE_READ_INTERVAL_MS, remainingMs)); - assertOperatorIntentCurrent(); - try { - const nextState = await readState(); - assertOperatorIntentCurrent(); - acceptState(nextState); - state = nextState; - } catch (readError) { - // A host Wi-Fi transition can briefly abort even a localhost fetch. - // Preserve the admitted operation and perform only the next bounded - // journal read; the original Apply is never reissued. - if (!(readError instanceof ApiError) || !readError.transportUnavailable) { - throw readError; - } - continue; - } - operation = operationByIdempotencyKey( - state, - "network.provision", - idempotencyKey, - ); - if (!operation) return state; - } - return state; -} const CONTROL_PHASE_WAIT_TIMEOUT_MS = 120_000; function controlPhase(state: XgridsK1State): XgridsApplicationControlPhase { @@ -642,93 +561,6 @@ function apiErrorForOperation( ); } -const CONNECT_SESSION_RESET_COPY = - "Автоматический повтор команды K1 не отправлялся. Сессия подключения в интерфейсе сброшена. Выполните новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз."; - -function resetConnectSessionMessage(detail: string): string { - return `${detail} ${CONNECT_SESSION_RESET_COPY}`; -} - -export function networkProvisionFailureMessage( - operation: XgridsOperation | null | undefined, -): string | null { - if (!operation || operation.status !== "failed") return null; - const code = operation.error?.code; - if (typeof code !== "string") return null; - - if (code === "BleakGATTProtocolError") { - const attCode = operation.error?.ble_att_error_code; - const attName = operation.error?.ble_att_error_name; - const attDetail = typeof attCode === "number" && typeof attName === "string" - ? ` Код периферии: ATT ${attCode} ${attName}.` - : ""; - if (operation.error?.device_write_attempted === false) { - return `Bluetooth-сеанс K1 завершился ошибкой до команды изменения сети.${attDetail} Запись сетевого профиля не выполнялась; выполните новый поиск после освобождения Bluetooth.`; - } - if (typeof attCode === "number" && typeof attName === "string") { - return resetConnectSessionMessage( - `Bluetooth-периферия завершила сетевую операцию ошибкой.${attDetail} Команда могла быть принята K1; итог текущей попытки не подтверждён.`, - ); - } - return resetConnectSessionMessage( - "Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён.", - ); - } - - if (code === "network-not-found") { - const attemptCount = operation.error?.scan_attempt_count; - const elapsedMs = operation.error?.scan_elapsed_ms; - const scanDetail = typeof attemptCount === "number" && typeof elapsedMs === "number" - ? ` macOS выполнила ${attemptCount} проверок за ${(elapsedMs / 1000).toFixed(1)} с.` - : ""; - return `K1 принял команду Quick Connect и подтвердил готовность точки доступа, но macOS не увидела её Wi‑Fi-сеть за отведённое время.${scanDetail} Quick Connect не установлен; автоматического повтора не было. Выполните новый поиск перед следующей явной попыткой или используйте Bridge.`; - } - - if ( - code === "keychain-authorization-required" - || code === "keychain-authorization-denied" - || code === "keychain-authorization-cancelled" - || code === "keychain-access-failed" - ) { - const failedBeforeDeviceWrite = operation.error?.side_effect_status === "none"; - return failedBeforeDeviceWrite - ? "Локальный профиль K1 недоступен в связке ключей. Команда устройству не отправлялась; подготовьте разрешение профиля отдельным действием и затем повторите подключение." - : resetConnectSessionMessage( - "После подтверждённого включения точки K1 локальный профиль стал недоступен в связке ключей. Дополнительный пароль не запрашивался; итог текущей попытки не подтверждён.", - ); - } - - const messages: Record = { - "host-wifi-operation-timeout": - resetConnectSessionMessage("Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён."), - "profile-ssid-mismatch": - "Сохранённый профиль относится к другому устройству. Подключение остановлено без повторной команды сканеру.", - "profile-credential-source-mismatch": - "Сохранённый профиль K1 не подтверждён для точной версии прошивки. Автоматического выбора другого пароля нет; подготовьте профиль отдельно перед новой попыткой.", - "profile-unavailable": - resetConnectSessionMessage("Локальный профиль выбранного K1 отсутствует; текущая попытка подключения завершилась ошибкой."), - "corewlan-error": - "macOS не смогла подключиться к точке доступа K1. Проверьте пароль сохранённой сети этого K1; автоматического повтора не было.", - "wifi-interface-unavailable": - "Системный Wi-Fi-интерфейс macOS недоступен. Команда сканеру автоматически не повторялась.", - "unsupported-platform": - "Для этой операционной системы адаптер подключения к точке K1 ещё не реализован.", - "credential-source-unavailable": - "Локальный профиль выбранного K1 не готов. Команда устройству не отправлялась; после подготовки профиля разрешена новая явная попытка.", - "network-provision-candidate-not-fresh": - "Результат Bluetooth-поиска отсутствует или устарел. Команда K1 не отправлялась; выполните один свежий поиск.", - "network-provision-candidate-changed": - "Bluetooth-кандидат изменился до команды K1. Записи не было; выполните один свежий поиск.", - "network-provision-candidate-name-unavailable": - "K1 не сообщил имя своей точки доступа. Команда устройству не отправлялась; выполните новый поиск.", - "network-provision-target-not-distinguishable-from-baseline": - resetConnectSessionMessage("После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён."), - "network-provision-lifecycle-busy": - "Сетевая операция заблокирована активной сессией или локальной очисткой. Команда K1 не отправлялась; завершите текущую сессию и повторите явно.", - }; - return messages[code] ?? null; -} - export function discoveryScanFailureMessage( operation: XgridsOperation | null | undefined, ): string | null { @@ -1138,7 +970,7 @@ export function useXgridsK1Runtime(enabled: boolean) { const operationId = newOperationId(); try { const nextState = await xgridsK1Api.scanBle({ - duration_seconds: options.durationSeconds ?? 6, + duration_seconds: options.durationSeconds ?? BLE_DISCOVERY_TIMEOUT_SECONDS, operation_id: operationId, expected_snapshot_runtime_id: expectedSnapshotRuntimeId(), }); @@ -1412,143 +1244,36 @@ export function useXgridsK1Runtime(enabled: boolean) { ); } }; - const previous = operationByIdempotencyKey( - state, - "network.provision", - request.idempotency_key, - ); - if (operationNeedsReconciliation(previous)) { - captureFailureReason(previous); - throw apiErrorForOperation( - resetConnectSessionMessage( - "Текущая попытка подключения не получила подтверждённого результата.", - ), - previous, - ); - } - if (operationAllowsFreshProvisioningIntent(previous)) { - intentDisposition = "release"; - captureFailureReason(previous); - const failureMessage = networkProvisionFailureMessage(previous); - throw apiErrorForOperation( - failureMessage - ?? "Подготовительный этап завершился до команды K1. После устранения причины разрешена новая явная попытка.", - previous, - ); - } - - let nextState: XgridsK1State; - if (previous?.status === "succeeded" && state) { - nextState = state; - } else try { - nextState = await xgridsK1Api.connect({ - ...request, + const observation = await observeProvisioningRequest(request, { + initialState: latestState.current, + send: (input) => xgridsK1Api.connect({ + ...input, expected_snapshot_runtime_id: expectedSnapshotRuntimeId(), - }); - } catch (connectError) { - // A failed network action is terminal and persisted by the backend. - // Re-read state only; never replay the device command. This replaces - // an opaque HTTP 502 banner with the exact host-association outcome. - let failedState: XgridsK1State; - try { - failedState = await xgridsK1Api.getState(); - } catch { - throw connectError; - } - acceptState(failedState); - failedState = await awaitNetworkProvisionSettlementAfterLostResponse( - failedState, - request.idempotency_key, - () => xgridsK1Api.getState(), - acceptState, - assertOperatorIntentCurrent, - ); - const failedOperation = operationByIdempotencyKey( - failedState, - "network.provision", - request.idempotency_key, - ); - // The response body can be lost after the backend has already - // committed the exact idempotent operation. The read-only journal is - // authoritative: acknowledge that success instead of presenting a - // false failure which could invite another operator click. - if (failedOperation?.status === "succeeded") { - nextState = failedState; - if ( - !exactAppliedNetworkIntentCompleted( - nextState, - request, - failedOperation, - ) - && !hasExactConnectionReady(nextState, request.connection_mode) - ) { - throw connectError; - } - } else { - captureFailureReason(failedOperation); - if (operationNeedsReconciliation(failedOperation)) { - throw apiErrorForOperation( - resetConnectSessionMessage( - "Текущая попытка подключения завершилась с неизвестным результатом.", - ), - failedOperation, - ); - } - if (operationAllowsFreshProvisioningIntent(failedOperation)) { - intentDisposition = "release"; - const failureMessage = networkProvisionFailureMessage(failedOperation); - throw apiErrorForOperation( - failureMessage - ?? "Подготовительный этап завершился до команды K1. После устранения причины разрешена новая явная попытка.", - failedOperation, - ); - } - const failureMessage = networkProvisionFailureMessage(failedOperation); - if (failureMessage) { - throw apiErrorForOperation(failureMessage, failedOperation); - } - if (failedOperation) { - throw apiErrorForOperation( - resetConnectSessionMessage( - "Текущая попытка подключения не получила подтверждённого результата.", - ), - failedOperation, - ); - } - throw connectError; - } - } - const operation = operationByIdempotencyKey( - nextState, - "network.provision", - request.idempotency_key, - ); - if (operationNeedsReconciliation(operation)) { - captureFailureReason(operation); - throw apiErrorForOperation( - resetConnectSessionMessage( - "Текущая попытка подключения завершилась с неизвестным результатом.", - ), - operation, - ); - } - if (operationAllowsFreshProvisioningIntent(operation)) { - intentDisposition = "release"; + }), + readState: () => xgridsK1Api.getState(), + acceptState, + currentState: () => latestState.current, + assertCurrent: assertOperatorIntentCurrent, + }); + // Return the same authoritative snapshot on failure and success. The + // form must not reconstruct an outcome from an empty result object. + observedState = observation.state; + let nextState = observation.state; + const operation = observation.operation; + if (operation?.status !== "succeeded") { captureFailureReason(operation); + intentDisposition = operationAllowsFreshProvisioningIntent(operation) + ? "release" + : "retain"; const failureMessage = networkProvisionFailureMessage(operation); - throw apiErrorForOperation( - failureMessage - ?? "Подготовительный этап завершился до команды K1. После устранения причины разрешена новая явная попытка.", - operation, - ); - } - if (operation && operation.status !== "succeeded") { - captureFailureReason(operation); - throw apiErrorForOperation( - resetConnectSessionMessage( - "Текущая попытка подключения завершилась без подтверждения успеха.", - ), - operation, + if (operation) { + throw apiErrorForOperation( + failureMessage ?? "Подключение не подтверждено. Проверьте состояние K1 или начните новое подключение; автоматического повтора не было.", + operation, + ); + } + throw observation.transportError ?? new ApiError( + "Сервис не подтвердил результат подключения. Обновите состояние; команда автоматически не повторяется.", ); } const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted( diff --git a/pyproject.toml b/pyproject.toml index b7b5b73..4d9e48d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ missioncore-plugin-sdk = { path = "packages/plugin-sdk", editable = true } [project.optional-dependencies] perception-stream = ["grpcio>=1.76,<2"] +node-device-media = ["aiortc==1.14.0"] [project.scripts] k1link = "k1link.device_plugins.xgrids_k1.cli:app" diff --git a/src/k1link/device_plugins/xgrids_k1/ble/scanner.py b/src/k1link/device_plugins/xgrids_k1/ble/scanner.py index 9d6efa6..c02733f 100644 --- a/src/k1link/device_plugins/xgrids_k1/ble/scanner.py +++ b/src/k1link/device_plugins/xgrids_k1/ble/scanner.py @@ -2,7 +2,10 @@ from __future__ import annotations import asyncio import math +import re +import sys from collections.abc import Callable +from contextlib import suppress from dataclasses import dataclass, replace from importlib.metadata import version from threading import Lock @@ -33,6 +36,8 @@ BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS = BLE_DISCOVERY_CANDIDATE_LEASE_TTL_SECONDS # low-level retained-handle lifetime. BLE_DISCOVERY_LEASE_TTL_SECONDS = BLE_RUNTIME_HANDLE_LEASE_TTL_SECONDS BLE_SCAN_HARD_TIMEOUT_GRACE_SECONDS = 5.0 +BLE_SCAN_INITIAL_WINDOW_SECONDS = 6.0 +BLE_SCAN_DEFAULT_TIMEOUT_SECONDS = 20.0 _runtime_handle_lock = Lock() _runtime_handles: dict[str, BLEDevice] = {} @@ -132,6 +137,14 @@ class BleDeviceRecord(TypedDict): k1_name_candidate: bool +class BleDiscoveryTiming(TypedDict, total=False): + scan_elapsed_ms: int + scanner_start_ms: int + initial_window_ms: int + first_candidate_ms: int + scan_extended: bool + + class BleScanResult(TypedDict): schema_version: int started_at_utc: str @@ -141,6 +154,7 @@ class BleScanResult(TypedDict): bleak_version: str device_count: int devices: list[BleDeviceRecord] + discovery_timing: BleDiscoveryTiming def advertisement_record(device: BLEDevice, advertisement: AdvertisementData) -> BleDeviceRecord: @@ -148,7 +162,7 @@ def advertisement_record(device: BLEDevice, advertisement: AdvertisementData) -> normalized_name = (name or "").casefold() return { "macos_uuid": device.address, - "id_kind": "corebluetooth_uuid", + "id_kind": "bluez_address" if sys.platform.startswith("linux") else "corebluetooth_uuid", "name": device.name, "local_name": advertisement.local_name, "rssi": advertisement.rssi, @@ -803,11 +817,34 @@ def connected_device_recovery_name( return normalized or None +async def _retrieve_bluez_device(address: str, details: object = None) -> BLEDevice | None: + """Retrieve only the exact cached BlueZ object; no discovery or connection.""" + if not re.fullmatch(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", address): + return None + try: + from bleak.backends.bluezdbus.manager import get_global_bluez_manager + + manager = await asyncio.wait_for(get_global_bluez_manager(), timeout=3) + path = details.get("path") if isinstance(details, dict) else None + if not path: + path = manager.get_default_adapter() + "/dev_" + address.upper().replace(":", "_") + if manager.get_device_address(path).casefold() != address.casefold(): + return None + name = manager.get_device_name(path) + return BLEDevice(address, name, details={"path": path, "props": { + "Address": address, "Alias": name, "Adapter": path.rsplit("/", 1)[0], + }}) + except Exception: + return None + + async def _retrieve_corebluetooth_device( captured: CapturedDiscoveredDevice, ) -> BLEDevice | None: """Retrieve one UUID through the exact CoreBluetooth manager that observed it.""" + if sys.platform.startswith("linux"): + return await _retrieve_bluez_device(captured.macos_uuid, captured.device.details) details = captured.device.details if not isinstance(details, tuple) or len(details) != 2: return None @@ -899,6 +936,20 @@ async def retrieve_known_device_capture_for_status_read( or runtime["poisoned"] ): return None + if sys.platform.startswith("linux"): + device = await _retrieve_bluez_device(macos_uuid) + after = ble_runtime_snapshot() + if (device is None or ble_runtime_owner_epoch_for_current_loop() != owner_epoch + or after["owner_epoch"] != owner_epoch + or not after["owner_loop_bound"] or after["poisoned"] + or after["active_operation_kind"] != "status-read"): + return None + now = _freshness_observed_at() + return CapturedDiscoveredDevice( + device=device, macos_uuid=macos_uuid, owner_epoch=owner_epoch, + scan_generation=0, captured_at_monotonic=now.monotonic, + captured_at_suspend_aware=now.suspend_aware, source="retrieved-durable", + ) context = _new_corebluetooth_retrieval_context(macos_uuid) if context is None: return None @@ -977,10 +1028,14 @@ async def discover_known_device_capture_for_status_read( or runtime["poisoned"] ): return None - try: - UUID(macos_uuid) - except (TypeError, ValueError): - return None + if sys.platform.startswith("linux"): + if not re.fullmatch(r"(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}", macos_uuid): + return None + else: + try: + UUID(macos_uuid) + except (TypeError, ValueError): + return None candidate = await BleakScanner.find_device_by_address( macos_uuid, @@ -1148,6 +1203,42 @@ def connected_device_recovery_snapshot( } +async def _discover_k1_advertisements( + *, timeout: float, diagnostics: BleDiscoveryTiming +) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Listen once, extending the initial window only while no K1 is visible. + + Every result and native handle comes from this scanner's callbacks. A name + match only ends discovery; it grants no identity or GATT authority. + """ + loop = asyncio.get_running_loop() + started = loop.time() + initial_window = min(timeout, BLE_SCAN_INITIAL_WINDOW_SECONDS) + discovered: dict[str, tuple[BLEDevice, AdvertisementData]] = {} + candidate_seen = asyncio.Event() + + def observe(device: BLEDevice, advertisement: AdvertisementData) -> None: + discovered[device.address] = (device, advertisement) + if advertisement_record(device, advertisement)["k1_name_candidate"]: + if not candidate_seen.is_set(): + diagnostics["first_candidate_ms"] = round((loop.time() - started) * 1000) + candidate_seen.set() + + diagnostics["initial_window_ms"] = round(initial_window * 1000) + diagnostics["scan_extended"] = False + async with BleakScanner(detection_callback=observe): + listening_started = loop.time() + diagnostics["scanner_start_ms"] = round((listening_started - started) * 1000) + await asyncio.sleep(initial_window) + remaining = max(0.0, timeout - (loop.time() - listening_started)) + if not candidate_seen.is_set() and remaining > 0: + diagnostics["scan_extended"] = True + with suppress(TimeoutError): + await asyncio.wait_for(candidate_seen.wait(), timeout=remaining) + diagnostics["scan_elapsed_ms"] = round((loop.time() - started) * 1000) + return discovered + + async def _scan_impl( duration_seconds: float, progress: BleOperationProgress, @@ -1163,7 +1254,10 @@ async def _scan_impl( progress.operation_stage = "discovery" scan_generation = _begin_scan_generation(owner_epoch) try: - discovered = await BleakScanner.discover(timeout=duration_seconds, return_adv=True) + discovery_timing: BleDiscoveryTiming = {} + discovered = await _discover_k1_advertisements( + timeout=duration_seconds, diagnostics=discovery_timing + ) handles = {device.address: device for device, _advertisement in discovered.values()} devices = [ advertisement_record(device, advertisement) @@ -1181,10 +1275,11 @@ async def _scan_impl( "started_at_utc": started_at, "completed_at_utc": utc_now_iso(), "duration_seconds": duration_seconds, - "adapter": "CoreBluetooth", + "adapter": "BlueZ" if sys.platform.startswith("linux") else "CoreBluetooth", "bleak_version": version("bleak"), "device_count": len(devices), "devices": devices, + "discovery_timing": discovery_timing, } except BaseException: _finish_failed_scan(scan_generation) diff --git a/src/k1link/device_plugins/xgrids_k1/camera.py b/src/k1link/device_plugins/xgrids_k1/camera.py index 224bd89..2f7460f 100644 --- a/src/k1link/device_plugins/xgrids_k1/camera.py +++ b/src/k1link/device_plugins/xgrids_k1/camera.py @@ -303,6 +303,7 @@ class XgridsK1CameraGateway: repository_root: Path, plugin_id: str, *, + evidence_root: Path | None = None, committed_segment_observer: CommittedCameraSegmentObserver | None = None, process_fence_descriptor_factory: Callable[[], int] | None = None, producer_stall_observer: CameraProducerStallObserver | None = None, @@ -316,6 +317,12 @@ class XgridsK1CameraGateway: if not 0.01 <= producer_watchdog_interval_seconds <= 60.0: raise ValueError("camera watchdog interval must be within 0.01..60 seconds") self._repository_root = repository_root.resolve() + # Runtime evidence may live outside a source checkout (worktrees/Node). + # The composition supplies this trusted root; request paths cannot widen it. + self._evidence_root = ( + evidence_root.expanduser().resolve() if evidence_root is not None + else self._repository_root + ) self._plugin_id = plugin_id self._lock = threading.RLock() self._lifecycle_lock = threading.Lock() @@ -674,8 +681,8 @@ class XgridsK1CameraGateway: root = session_dir.expanduser().resolve() if not root.is_dir(): raise ValueError("observation session directory does not exist") - if not root.is_relative_to(self._repository_root): - raise ValueError("camera recording root must stay inside the repository") + if not root.is_relative_to(self._evidence_root): + raise ValueError("camera recording root must stay inside the configured evidence root") reserved_generation: list[int] = [] def reserve() -> bool: @@ -805,8 +812,8 @@ class XgridsK1CameraGateway: root = session_dir.expanduser().resolve() if not root.is_dir(): raise ValueError("observation session directory does not exist") - if not root.is_relative_to(self._repository_root): - raise ValueError("camera recording root must stay inside the repository") + if not root.is_relative_to(self._evidence_root): + raise ValueError("camera recording root must stay inside the configured evidence root") with self._lifecycle_lock: with self._lock: diff --git a/src/k1link/device_plugins/xgrids_k1/facade.py b/src/k1link/device_plugins/xgrids_k1/facade.py index db7fab4..0f75f9e 100644 --- a/src/k1link/device_plugins/xgrids_k1/facade.py +++ b/src/k1link/device_plugins/xgrids_k1/facade.py @@ -88,6 +88,7 @@ from k1link.device_plugins.xgrids_k1.ble.runtime_arbiter import ( wait_for_ble_runtime_idle, ) from k1link.device_plugins.xgrids_k1.ble.scanner import ( + BLE_SCAN_DEFAULT_TIMEOUT_SECONDS, BLE_SCAN_HARD_TIMEOUT_GRACE_SECONDS, CapturedDiscoveredDevice, capture_discovered_device, @@ -226,6 +227,7 @@ from k1link.device_plugins.xgrids_k1.viewer.runtime import ( VisualizationRuntime, new_live_session_dir, ) +from k1link.device_plugins.xgrids_k1.wifi_failure import reviewed_station_failure_code from k1link.host_network import ( HostWifiAssociationIdentityProbe, HostWifiAssociationIdentityResult, @@ -1014,7 +1016,7 @@ class _ConfiguredEndpointHostObservation: class BleScanRequest(StrictRequest): - duration_seconds: float = Field(default=6.0, ge=1.0, le=60.0) + duration_seconds: float = Field(default=BLE_SCAN_DEFAULT_TIMEOUT_SECONDS, ge=1.0, le=60.0) operation_id: str | None = Field(default=None, min_length=1, max_length=128) @@ -1476,6 +1478,7 @@ class XgridsK1CompatibilityService: application_authority_loader: ApplicationAuthorityLoader | None = None, calibration_snapshot_reader: DeviceCalibrationSnapshotReader | None = None, host_wifi_association_probe: HostWifiAssociationProbe | None = None, + visualization_bridge_factory: Callable[..., Any] | None = None, ) -> None: self.repository_root = repository_root.resolve() # Every CoreBluetooth entrypoint, including read-only calibration and @@ -1876,6 +1879,7 @@ class XgridsK1CompatibilityService: physical_command_coordinator=self._physical_command_coordinator, ) self.runtime = VisualizationRuntime( + bridge_factory=visualization_bridge_factory, normalizer=normalize_k1_message, message_observer=self._observe_runtime_message, published_envelope_observer=self._observe_published_runtime_envelope, @@ -1886,6 +1890,7 @@ class XgridsK1CompatibilityService: self.camera_preview = XgridsK1CameraGateway( self.repository_root, XGRIDS_K1_PLUGIN_ID, + evidence_root=self.evidence_root, committed_segment_observer=self._observe_committed_camera_segment, process_fence_descriptor_factory=(self._duplicate_camera_process_fence_descriptor), producer_stall_observer=self._observe_camera_producer_stall, @@ -10190,19 +10195,27 @@ class XgridsK1CompatibilityService: f"Поиск завершён. Найдено BLE-устройств: {len(devices)}." ) self._operation_phase = None + discovery_result = { + "candidate_count": len(devices), + "likely_k1_candidate_count": sum(item["likely_k1"] is True for item in devices), + "duration_seconds": duration_seconds, + "discovery_generation": scan_generation, + **result.get("discovery_timing", {}), + } + logger.info( + "K1 BLE discovery completed", + extra={ + "event_code": "k1_ble_discovery_completed", + "operation_id": operation.operation_id, + **discovery_result, + }, + ) self._operations.transition( operation.operation_id, "succeeded", stage_code="completed", message_code="discovery.scan.completed", - result={ - "candidate_count": len(devices), - "likely_k1_candidate_count": sum( - item["likely_k1"] is True for item in devices - ), - "duration_seconds": duration_seconds, - "discovery_generation": scan_generation, - }, + result=discovery_result, ) with self._lock: if ( @@ -12143,6 +12156,14 @@ class XgridsK1CompatibilityService: safe_to_retry=not device_write_attempted, host_boundary="corebluetooth" if isinstance(exc, BleakError) else None, ) + station_failure_code = reviewed_station_failure_code( + operation_error, + firmware_version=request.compatibility_attestation.firmware_version, + connection_mode=request.connection_mode, + ) + if station_failure_code is not None: + operation_error["transport_error_code"] = operation_error["code"] + operation_error["code"] = station_failure_code if idempotency_record.stage == "prepared": cancelled = isinstance(exc, asyncio.CancelledError) idempotency_record = idempotency_journal.complete( @@ -12222,6 +12243,7 @@ class XgridsK1CompatibilityService: "connection_mode": request.connection_mode, "error_category": operation_error["category"], "error_code": operation_error["code"], + "transport_error_code": operation_error.get("transport_error_code"), "safe_to_retry": operation_error["safe_to_retry"], "side_effect_status": operation_error["side_effect_status"], "network_change_attempted": device_write_attempted, @@ -35803,6 +35825,14 @@ def _inspect_host_path(target: str) -> HostPathProbeResult: if separator: fields[key] = value.strip() + if sys.platform.startswith("linux"): + from .linux_host import route_fields + + try: + fields = route_fields(target) + except (OSError, ValueError, subprocess.SubprocessError): + inspection_reason = "host-route-inspection-unavailable" + interface = fields.get("interface") or None destination = fields.get("destination", "") gateway = fields.get("gateway", "") @@ -35825,6 +35855,10 @@ def _host_route_class(target: str) -> str: """Classify the host route without transmitting packets to the target.""" target = validate_private_ipv4(target) + if sys.platform.startswith("linux"): + path = _inspect_host_path(target) + return {"direct": "direct-or-routed", "default": "default-route", + "tunnel": "tunnel"}.get(path.route_class, "unknown") if sys.platform != "darwin": return "unknown" try: @@ -35874,6 +35908,7 @@ def _classify_host_route( "wireguard", "gif", "stf", + "tailscale", ) direct_lan_prefixes = ( "en", @@ -35882,6 +35917,7 @@ def _classify_host_route( "vlan", "usb", "p2p", + "wl", ) if normalized_interface.startswith(tunnel_prefixes): return "tunnel", "host-route-tunnel" diff --git a/src/k1link/device_plugins/xgrids_k1/linux_host.py b/src/k1link/device_plugins/xgrids_k1/linux_host.py new file mode 100644 index 0000000..fad2323 --- /dev/null +++ b/src/k1link/device_plugins/xgrids_k1/linux_host.py @@ -0,0 +1,214 @@ +"""Read-only Linux host adapters for the reviewed K1 Bridge workflow. + +No host association or subnet discovery is available through this module. +""" + +from __future__ import annotations + +import hashlib +import hmac +import json +import os +import re +import secrets +import stat +import subprocess +from pathlib import Path + +from k1link.device_plugins.xgrids_k1.protocol.application_authority import ( + ApplicationAuthorityLoadError, + ApplicationAuthoritySourceSnapshot, +) +from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import ( + ApplicationControlAuthority, +) + + +def _run(args: list[str], timeout: float = 8) -> str: + result = subprocess.run(args, capture_output=True, text=True, timeout=timeout, check=False) + if result.returncode: + # Never forward subprocess diagnostics: SSIDs or other local data may occur. + raise OSError("Network observation unavailable") + if len(result.stdout) > 131072: + raise OSError("Network observation exceeded its bound") + return result.stdout + + +def nm_fields(line: str) -> list[str]: + """Parse nmcli terse escaping, including SSIDs containing colons/backslashes.""" + fields, value, escaped = [], "", False + for char in line: + if escaped: + value += char + escaped = False + elif char == "\\": + escaped = True + elif char == ":": + fields.append(value) + value = "" + else: + value += char + if escaped: + value += "\\" + return [*fields, value] + + +def wifi_networks() -> list[dict]: + text = _run( + [ + "nmcli", + "-t", + "--escape", + "yes", + "-f", + "SSID,SIGNAL,SECURITY", + "device", + "wifi", + "list", + "--rescan", + "yes", + ], + timeout=20, + ) + networks = {} + for line in text.splitlines(): + fields = nm_fields(line) + if len(fields) != 3: + continue + ssid, strength, security = fields + if not ssid or not 1 <= len(ssid.encode()) <= 32 or not strength.isdigit(): + continue + signal = min(100, max(0, int(strength))) + if signal >= networks.get(ssid, {}).get("signal", -1): + networks[ssid] = {"ssid": ssid, "signal": signal, "security": security} + return sorted(networks.values(), key=lambda v: (-v["signal"], v["ssid"]))[:64] + + +def route_fields(target: str) -> dict[str, str]: + # fibmatch returns the actual matched route (including "default"), whereas + # an ordinary `route get` resolves dst to the host even for a default route. + routes = json.loads(_run(["ip", "-j", "route", "get", target, "fibmatch"])) + if not isinstance(routes, list) or len(routes) != 1: + raise OSError("Ambiguous kernel route") + route = routes[0] + if route.get("type", "unicast") != "unicast" or not route.get("dev"): + raise OSError("No unicast route") + return { + "interface": str(route["dev"]), + "destination": str(route.get("dst", "")), + "gateway": str(route.get("gateway", "")), + } + + +class LinuxWifiAssociationProbe: + def __init__(self, sys_net: Path = Path("/sys/class/net")): + self.sys_net = sys_net + self.key = secrets.token_bytes(32) + + def observe(self, *, interface_name: str | None, timeout_seconds: float = 30) -> dict: + result = { + "schema_version": 1, + "adapter": "linux-networkmanager", + "wifi_interface": None, + "association_state": "unavailable", + "evidence_quality": "unavailable", + "continuity_proven": False, + "continuity_token": "", + "reason_code": "host-wifi-observation-unavailable", + } + if not interface_name or not re.fullmatch(r"[A-Za-z0-9_.:-]{1,64}", interface_name): + return result + interface = self.sys_net / interface_name + if not interface.exists(): + return result + wireless = (interface / "wireless").exists() or (interface / "phy80211").exists() + result["wifi_interface"] = wireless + if not wireless: + result.update( + association_state="not-wifi", + evidence_quality="not-wifi", + continuity_proven=True, + reason_code=None, + ) + material = "not-wifi:" + interface_name + else: + try: + lines = _run( + [ + "nmcli", + "-t", + "--escape", + "yes", + "-f", + "ACTIVE,SSID,BSSID", + "device", + "wifi", + "list", + "ifname", + interface_name, + "--rescan", + "no", + ], + timeout=min(timeout_seconds, 8), + ) + active = [nm_fields(line) for line in lines.splitlines() if line.startswith("yes:")] + if len(active) != 1 or len(active[0]) != 3 or not active[0][2]: + return result + except (OSError, subprocess.SubprocessError): + return result + result.update( + association_state="associated", + evidence_quality="ssid+bssid", + continuity_proven=True, + reason_code=None, + ) + material = json.dumps([interface_name, *active[0][1:]], ensure_ascii=False) + result["continuity_token"] = hmac.new( + self.key, material.encode(), hashlib.sha256 + ).hexdigest() + return result + + +class LinuxApplicationAuthorityLoader: + """Read only a systemd-delivered credential, never an argv/env secret. + + Provisioning of the encrypted credential is a separate privileged install + operation. Missing authority does not prevent discovery but prevents control. + """ + + def __init__(self, directory: Path | None = None): + self.directory = directory + + def snapshot(self) -> ApplicationAuthoritySourceSnapshot: + return ApplicationAuthoritySourceSnapshot( + provider="linux-systemd-credential", + service="mission-core-k1.service", + account="k1-application", + ) + + def load(self) -> ApplicationControlAuthority: + directory = self.directory or Path( + os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials/mission-core-k1.service") + ) + buffer = bytearray() + try: + fd = os.open(directory / "k1-application", os.O_RDONLY | os.O_NOFOLLOW) + with os.fdopen(fd, "rb") as stream: + info = os.fstat(stream.fileno()) + if ( + not stat.S_ISREG(info.st_mode) + or info.st_uid != os.geteuid() + or info.st_mode & 0o077 + ): + raise ValueError("Invalid credential permissions") + buffer.extend(stream.read(1025)) + if len(buffer) > 1024: + raise ValueError("Credential too large") + return ApplicationControlAuthority(openapi_key=buffer.decode("ascii").strip()) + except (OSError, ValueError, UnicodeError): + raise ApplicationAuthorityLoadError( + "Служебный ключ K1 не установлен на БК.", + reason_code="application_authority_unavailable", + ) from None + finally: + buffer[:] = b"\0" * len(buffer) diff --git a/src/k1link/device_plugins/xgrids_k1/node_bridge.py b/src/k1link/device_plugins/xgrids_k1/node_bridge.py new file mode 100644 index 0000000..a192eee --- /dev/null +++ b/src/k1link/device_plugins/xgrids_k1/node_bridge.py @@ -0,0 +1,248 @@ +"""Node-owned, Bridge-only adapter around the admitted K1 plugin runtime. + +The local Unix worker accepts neither host-Wi-Fi actions nor arbitrary plugin +invocations. Its public projection contains no credentials or raw evidence. +""" + +from __future__ import annotations + +import asyncio +from datetime import UTC, datetime +from pathlib import Path + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from missioncore_plugin_sdk.v0alpha2.runtime import RuntimeActionInvocation + +from k1link.viewer.node_rerun import NodeRerunHub + +from .ble.scanner import BLE_SCAN_DEFAULT_TIMEOUT_SECONDS +from .facade import ( + XGRIDS_K1_PLUGIN_ID, + XgridsK1CompatibilityService, + XgridsK1PluginFacade, + _validate_installed_compatibility_profile, +) +from .linux_host import LinuxApplicationAuthorityLoader, LinuxWifiAssociationProbe, wifi_networks + +ATTESTATION = { + "firmware_version": "3.0.2", + "topology": "direct-lan", + "verification": "live-device-info", +} + + +class NodeBridge: + def __init__(self, repository_root: Path, *, service=None): + self.rerun = NodeRerunHub() + if service is None: + _validate_installed_compatibility_profile(repository_root) + service = XgridsK1CompatibilityService( + repository_root, + application_authority_loader=LinuxApplicationAuthorityLoader(), + host_wifi_association_probe=LinuxWifiAssociationProbe(), + visualization_bridge_factory=self.rerun.create, + ) + self.service = service + self.facade = XgridsK1PluginFacade(service) + self.lock = asyncio.Lock() + + async def state(self) -> dict: + snapshot = await self.invoke("state.read", {}, "state-read") + return self.project(snapshot) + + @staticmethod + def project(snapshot: dict) -> dict: + lifecycle = snapshot.get("connection_lifecycle", {}) + return { + "available": True, + "model": "XGRIDS K1", + "mode": "bridge", + "runtime_id": snapshot["snapshot_runtime_id"], + "discovery_generation": snapshot.get("ble_discovery_generation", 0), + "mode_revision": snapshot.get("desired_connection_mode_revision", 0), + "candidates": [ + {"id": v["device_id"], "name": v.get("name") or "K1", "rssi": v.get("rssi")} + for v in snapshot.get("devices", []) + if v.get("likely_k1") + ][:32], + "connected": lifecycle.get("connection_ready") is True + and snapshot.get("active_connection_mode") == "bridge", + "ready_to_start": lifecycle.get("ready_to_start") is True, + "selected_device_id": snapshot.get("selected_device_id"), + "device_session": snapshot.get("device_session"), + "ip": snapshot.get("k1_ip"), + "phase": snapshot.get("phase"), + "observed_at": datetime.now(UTC).isoformat(), + } + + async def invoke(self, action: str, parameters: dict, identifier: str) -> dict: + return await self.facade.invoke( + RuntimeActionInvocation( + invocation_id=identifier, + plugin_id=XGRIDS_K1_PLUGIN_ID, + action_id=action, + requested_at=datetime.now(UTC), + parameters=parameters, + ) + ) + + async def execute(self, command: dict) -> dict: + action, identifier = command["action"], command["operation_id"] + parameters = command.get("parameters", {}) + if action not in {"scan", "networks", "connect", "verify"}: + raise ValueError("Unsupported Node device action") + async with self.lock: + if datetime.fromisoformat(command["deadline_at"]) <= datetime.now(UTC): + raise ValueError("Command expired before dispatch") + state = await self.state() + if command.get("runtime_id") != state["runtime_id"]: + raise ValueError("Node runtime changed") + if action == "networks": + return {**state, "networks": await asyncio.to_thread(wifi_networks)} + if action == "scan": + result = await self.invoke( + "discovery.scan", + { + "duration_seconds": BLE_SCAN_DEFAULT_TIMEOUT_SECONDS, + "operation_id": identifier, + }, + identifier, + ) + else: + # Do not derive fences at dispatch: they are the exact version + # shown to the operator before credentials were submitted. + if ( + parameters.get("discovery_generation") != state["discovery_generation"] + or parameters.get("mode_revision") != state["mode_revision"] + or parameters.get("device_id") not in {v["id"] for v in state["candidates"]} + ): + raise ValueError("Device selection changed") + payload = { + "device_id": parameters["device_id"], + "compatibility_attestation": ATTESTATION, + "operation_id": identifier, + "expected_mode_revision": parameters["mode_revision"], + "expected_discovery_generation": parameters["discovery_generation"], + "expected_snapshot_runtime_id": state["runtime_id"], + } + if action == "connect": + payload.update( + connection_mode="bridge", + allow_host_wifi_switch=False, + idempotency_key=identifier, + ssid=parameters.get("ssid"), + password=parameters.get("password"), + ) + try: + result = await self.invoke( + "network.provision" if action == "connect" else "connection.verify", + payload, + identifier, + ) + finally: + payload.pop("password", None) + parameters.pop("password", None) + return self.project(result) + + +def create_app(repository_root: Path): + from contextlib import asynccontextmanager + + bridge = NodeBridge(repository_root) + from k1link.viewer.node_media import NodeMediaPeers + + from .node_sensor import NodeK1Sensor + + peers = NodeMediaPeers(bridge.rerun, bridge.service.camera_preview) + sensor = NodeK1Sensor(bridge, peers) + + @asynccontextmanager + async def lifespan(_app): + yield + await peers.close_all() + await asyncio.to_thread(bridge.service.close) + + app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None, lifespan=lifespan) + + @app.get("/status") + async def status(): + return await bridge.state() + + @app.get("/inventory") + async def inventory(request: Request): + return await sensor.inventory(request.headers["X-Node-Id"]) + + @app.get("/prepare-safe") + async def prepare_safe(): + state = await sensor.raw_state() + acquisition = state.get("acquisition") or {} + control = state.get("application_control_session") or {} + physical = control.get("physical_command") or state.get("physical_command") or {} + return { + "safe": not bridge.lock.locked() + and state.get("source_mode") == "idle" + and acquisition.get("state") in {None, "completed", "failed", "aborted"} + and not physical.get("requires_reconciliation") + and control.get("state") + not in {"start-requested", "initializing", "scanning", "stop-requested"} + } + + @app.post("/sensor-operation") + async def sensor_operation(request: Request): + try: + data = await request.body() + if len(data) > 65536: + raise ValueError("Request too large") + command = await request.json() + if datetime.fromisoformat(command["deadline_at"]) <= datetime.now(UTC): + raise ValueError("Command expired") + result = await sensor.execute(command, request.headers["X-Node-Id"]) + return {"state": "complete", "result": result} + except Exception: + return { + "state": "unknown", + "error": "Действие K1 не подтверждено. Обновите состояние устройства.", + } + + @app.post("/operation") + async def operation(request: Request): + # The Go broker admits size/action/binding/deadline and journals the + # operation before calling this private Unix socket. Never serialize + # Pydantic validation errors, exception text or the incoming payload. + body = {} + try: + data = await request.body() + if len(data) > 16384: + raise ValueError("Request too large") + body = await request.json() + return await bridge.execute(body) + except Exception: + return JSONResponse( + { + "error": ( + "Действие K1 не подтверждено. Обновите состояние; " + "проверьте питание, Bluetooth и сеть БК." + ) + }, + status_code=409, + ) + finally: + if isinstance(body, dict) and isinstance(body.get("parameters"), dict): + body["parameters"].pop("password", None) + + return app + + +def main(): + import os + + import uvicorn + + os.umask(0o007) + app = create_app(Path("/usr/lib/mission-core-node/k1")) + uvicorn.run(app, uds="/run/mission-core-k1/driver.sock", access_log=False, log_level="warning") + + +if __name__ == "__main__": + main() diff --git a/src/k1link/device_plugins/xgrids_k1/node_sensor.py b/src/k1link/device_plugins/xgrids_k1/node_sensor.py new file mode 100644 index 0000000..538e510 --- /dev/null +++ b/src/k1link/device_plugins/xgrids_k1/node_sensor.py @@ -0,0 +1,255 @@ +"""SDK projection and explicit acquisition actions for the Node-owned K1.""" + +import asyncio +import hashlib +import time +from datetime import UTC, datetime + +from .facade import ( + XGRIDS_K1_MODEL_ID, + XGRIDS_K1_PLUGIN_ID, + XGRIDS_K1_PLUGIN_VERSION, + ViewerSettingsRequest, +) +from .node_bridge import ATTESTATION + +PHYSICAL_ACCEPTANCE_KEYS = ( + "operator_present", + "owner_controlled_device", + "lixelgo_closed", + "battery_storage_confirmed", + "expected_physical_state_confirmed", +) + + +def project_sensor(snapshot, node_id): + session = snapshot.get("device_session") + if not session: + return None + identifier = "k1_" + hashlib.sha256(session["device_id"].encode()).hexdigest()[:32] + lifecycle = snapshot.get("connection_lifecycle", {}) + connected = ( + lifecycle.get("connection_ready") is True + and snapshot.get("active_connection_mode") == "bridge" + ) + acquisition = snapshot.get("acquisition") or {} + acquisition_state = { + "running": "streaming", + "active": "streaming", + "prepared": "preparing", + "starting": "starting", + "stopping": "stopping", + "failed": "failed", + }.get(acquisition.get("state"), "idle") + if snapshot.get("source_mode") == "live": + acquisition_state = "streaming" + now = datetime.now(UTC).isoformat() + context = { + "session_id": session["device_session_id"], + "device": { + "device_id": identifier, + "model": { + "plugin_id": XGRIDS_K1_PLUGIN_ID, + "plugin_version": XGRIDS_K1_PLUGIN_VERSION, + "model_id": XGRIDS_K1_MODEL_ID, + }, + "stability": "provisional", + "basis": "plugin-derived", + }, + "execution": { + "node_id": node_id, + "agent_instance_id": snapshot["snapshot_runtime_id"], + "platform": "linux", + }, + "opened_at": session["opened_at"], + } + control = snapshot.get("application_control_session") or {} + return { + "id": identifier, + "name": "XGRIDS K1", + "model": "XGRIDS K1", + "kind": "k1", + "prepared": True, + "configured": True, + "verified": connected, + "online": connected, + "usb": "", + "connection_label": "Wi-Fi · " + (snapshot.get("k1_ip") or "—"), + "layers": ["points", "camera"], + "snapshot": { + "context": context, + "revision": snapshot["snapshot_revision"], + "enrollment": "enrolled", + "connectivity": "connected" if connected else "offline", + "acquisition": acquisition_state, + "observed_at": now, + }, + "control": { + "generation": control.get("session_generation"), + "revision": control.get("state_revision"), + "phase": control.get("state"), + "can_start": lifecycle.get("ready_to_start", False), + "can_stop": control.get("state") in {"start-requested", "initializing", "scanning"}, + "acquisition_id": acquisition.get("acquisition_id"), + }, + "live_settings": snapshot.get("viewer_settings", {}), + "frames": snapshot.get("metrics", {}), + "recordings": [], + } + + +class NodeK1Sensor: + def __init__(self, bridge, peers): + self.bridge, self.peers = bridge, peers + + async def raw_state(self): + return await self.bridge.invoke("state.read", {}, "sensor-state") + + async def inventory(self, node_id): + state = await self.raw_state() + item = project_sensor(state, node_id) + return {"items": [item] if item else []} + + @staticmethod + def cas(state, *, acquisition=True): + control = state["application_control_session"] + if acquisition: + return { + "expected_control_session_generation": control["session_generation"], + "expected_control_state_revision": control["state_revision"], + } + return { + "expected_session_generation": control["session_generation"], + "expected_state_revision": control["state_revision"], + } + + async def execute(self, command, node_id): + state = await self.raw_state() + item = project_sensor(state, node_id) + if ( + not item + or command["session"]["device_id"] != item["id"] + or command["session"]["session_id"] != item["snapshot"]["context"]["session_id"] + ): + raise ValueError("Device session changed") + action, params, identifier = ( + command["action_id"], + command.get("parameters", {}), + command["operation_id"], + ) + if action == "details": + return item + if action == "close-peer": + await self.peers.close(params.get("peer_id")) + return {"ok": True} + if action == "offer": + if item["snapshot"]["acquisition"] != "streaming": + raise ValueError("Acquisition is not active") + return await self.peers.offer(params) + async with self.bridge.lock: + if datetime.fromisoformat(command["deadline_at"]) <= datetime.now(UTC): + raise ValueError("Command expired before dispatch") + state = await self.raw_state() + current = project_sensor(state, node_id) + if ( + current is None + or current["snapshot"]["context"]["session_id"] != command["session"]["session_id"] + ): + raise ValueError("Device session changed") + runtime = state["snapshot_runtime_id"] + if action == "option": + if ( + set(params) != {"profile", "settings"} + or params["profile"] != "live-acquisition" + ): + raise ValueError("Only the live profile belongs to this device") + settings = ViewerSettingsRequest.model_validate(params["settings"]) + result = await self.bridge.invoke( + "viewer.settings.update", settings.model_dump(), identifier + ) + return project_sensor(result, node_id) + if action == "verify": + result = await self.bridge.invoke( + "connection.verify", {"expected_snapshot_runtime_id": runtime}, identifier + ) + return project_sensor(result, node_id) + if action not in {"start", "stop"} or params.get("operator_confirmed") is not True: + raise ValueError("Unsupported device action") + if ( + params.get("control_generation") != current["control"]["generation"] + or params.get("acquisition_id") != current["control"]["acquisition_id"] + ): + raise ValueError("Control session changed") + # Same explicit START/STOP acceptance as the admitted local plugin. + # No confirmation is inferred from polling, discovery or a preview offer. + acceptance = dict.fromkeys(PHYSICAL_ACCEPTANCE_KEYS, True) + if action == "stop": + result = await self.bridge.invoke( + "acquisition.stop", + { + "operation_id": identifier, + "idempotency_key": identifier, + "acquisition_id": params.get("acquisition_id"), + "mode": "graceful", + "physical_acceptance": acceptance, + "expected_snapshot_runtime_id": runtime, + **self.cas(state), + }, + identifier, + ) + await self.peers.close_all() + return project_sensor(result, node_id) + deadline = min( + datetime.fromisoformat(command["deadline_at"]).timestamp(), time.time() + 165 + ) + dispatched = set() + while time.time() < deadline: + if state["snapshot_runtime_id"] != runtime: + raise ValueError("Runtime changed during START") + control = state.get("application_control_session") or {} + phase = control.get("state") + physical = control.get("physical_command") or state.get("physical_command") or {} + if physical.get("requires_reconciliation"): + raise ValueError("Physical state requires explicit reconciliation") + acquisition = state.get("acquisition") or {} + payload = {"expected_snapshot_runtime_id": runtime} + next_action = None + if phase == "connection-ready": + if control.get("inspection_only"): + next_action = "application-control.session.open" + payload.update(acceptance, timezone_name="UTC") + else: + next_action = "application-control.workspace.enter" + payload.update(self.cas(state, acquisition=False), operator_confirmed=True) + elif phase == "workspace-ready" and acquisition.get("state") != "prepared": + next_action = "acquisition.prepare" + payload.update( + self.cas(state), + operation_id=identifier + "_prepare", + idempotency_key=identifier + "_prepare", + project_name="node-" + identifier[3:15], + compatibility_attestation=ATTESTATION, + ) + elif phase == "project-ready" and acquisition.get("state") == "prepared": + next_action = "acquisition.start" + payload.update( + self.cas(state), + operation_id=identifier, + idempotency_key=identifier, + acquisition_id=acquisition["acquisition_id"], + expected_state_revision=acquisition["state_revision"], + physical_acceptance=acceptance, + ) + elif phase in {"failed", "idle", "closed", "completed"}: + raise ValueError("K1 control not ready") + elif phase in {"start-requested", "initializing", "scanning"}: + return project_sensor(state, node_id) + if next_action and next_action not in dispatched: + dispatched.add(next_action) + state = await self.bridge.invoke( + next_action, payload, identifier + "_" + str(len(dispatched)) + ) + else: + await asyncio.sleep(0.5) + state = await self.raw_state() + raise TimeoutError("K1 did not confirm START") diff --git a/src/k1link/device_plugins/xgrids_k1/wifi_failure.py b/src/k1link/device_plugins/xgrids_k1/wifi_failure.py new file mode 100644 index 0000000..f0a514f --- /dev/null +++ b/src/k1link/device_plugins/xgrids_k1/wifi_failure.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from collections.abc import Mapping + + +def reviewed_station_failure_code( + error: Mapping[str, object], *, firmware_version: str, connection_mode: str +) -> str | None: + """Interpret the reviewed FW 3.0.2 station callback, retaining ATT evidence. + + lixel_nman's wifi_connect return value is passed to the GATT write result. + Its 4 (SSID not found) and 6 (credentials required) collide with standard + ATT names. This is a profile-scoped diagnosis, not network-state proof or + retry authority. See the 2026-09-06 firmware callback audit. + """ + if not ( + firmware_version == "3.0.2" + and connection_mode in {"bridge", "direct-connect"} + and error.get("code") == "BleakGATTProtocolError" + and error.get("operation_stage") == "gatt-write" + and error.get("device_write_attempted") is True + and error.get("device_write_confirmed") is False + and error.get("resolved_write_mode") == "with_response" + and error.get("frame_length") == 99 + ): + return None + code = error.get("ble_att_error_code") + if not isinstance(code, int) or isinstance(code, bool): + return None + return { + 4: "k1-wifi-network-not-found", + 6: "k1-wifi-credentials-required", + }.get(code) diff --git a/src/k1link/fleet/device_enrollment.py b/src/k1link/fleet/device_enrollment.py new file mode 100644 index 0000000..f32c970 --- /dev/null +++ b/src/k1link/fleet/device_enrollment.py @@ -0,0 +1,173 @@ +"""Bounded, transient delivery of Node-owned wireless device operations. + +Unlike ordinary sensor operations these can contain a WLAN password. The +pending payload never enters Fleet's database, listing, event stream or result. +The Node owns the durable, redacted operation journal and at-most-once dispatch. +""" + +import copy +import json +import re +import time +from datetime import UTC, datetime + +from .trust import PairingError + +ACTIONS = {"scan", "networks", "connect", "verify"} + + +def validate(value): + if not isinstance(value, dict) or set(value) != { + "operation_id", + "node_id", + "runtime_id", + "action", + "deadline_at", + "parameters", + }: + raise PairingError("Неверный запрос подключения устройства.") + if ( + not re.fullmatch(r"op_[0-9a-f]{32}", str(value["operation_id"])) + or value["action"] not in ACTIONS + or len(json.dumps(value)) > 16384 + or not isinstance(value["runtime_id"], str) + or not value["runtime_id"] + or len(value["runtime_id"]) > 128 + ): + raise PairingError("Неподдерживаемая операция подключения.") + try: + delta = (datetime.fromisoformat(value["deadline_at"]) - datetime.now(UTC)).total_seconds() + if not 0 < delta <= 180: + raise ValueError + except (ValueError, TypeError): + raise PairingError("Срок запроса истёк. Обновите устройства.") from None + parameters = value["parameters"] + expected = {"device_id", "discovery_generation", "mode_revision"} + if value["action"] == "connect": + expected |= {"ssid", "password"} + elif value["action"] in {"scan", "networks"}: + expected = set() + if not isinstance(parameters, dict) or set(parameters) != expected: + raise PairingError("Неверные параметры подключения.") + if "device_id" in expected and ( + not isinstance(parameters["device_id"], str) + or not 1 <= len(parameters["device_id"]) <= 128 + or type(parameters["discovery_generation"]) is not int + or type(parameters["mode_revision"]) is not int + or min(parameters["discovery_generation"], parameters["mode_revision"]) < 0 + ): + raise PairingError("Обновите выбранное устройство.") + if value["action"] == "connect" and any( + not isinstance(parameters[key], str) or not 1 <= len(parameters[key].encode()) <= limit + for key, limit in (("ssid", 32), ("password", 64)) + ): + raise PairingError("Проверьте название сети и пароль.") + + +class DeviceEnrollment: + def __init__(self): + # Access only under the enclosing FleetRegistry.lock. + self.pending = {} + + def prune(self): + now = time.time() + for key, entry in list(self.pending.items()): + if now - entry["created"] > 600: + del self.pending[key] + elif entry["deadline"] <= now and entry["public"]["state"] in {"queued", "running"}: + entry["payload"] = None + entry["public"] = { + "operation_id": key[1], + "state": "unknown", + "error": "Подтверждение с БК не получено. Обновите состояние K1.", + } + + def submit(self, fleet, vehicle_id, value): + validate(value) + with fleet.lock: + self.prune() + row = fleet.find(vehicle_id) + state = row.get("device_enrollment", {}) + if ( + fleet.public(row)["connectivity"] != "online" + or value["node_id"] != row["node_id"] + or value["runtime_id"] != state.get("runtime_id") + or state.get("available") is not True + ): + raise PairingError("БК или служба K1 недоступны. Обновите устройства.") + key = (vehicle_id, value["operation_id"]) + existing = self.pending.get(key) + if existing: + # First intent owns this ID; never accept replacement secrets. + return copy.deepcopy(existing["public"]) + if len(self.pending) >= 32: + raise PairingError("Слишком много запросов. Повторите позже.") + public = {"operation_id": value["operation_id"], "state": "queued"} + self.pending[key] = { + "public": public, + "payload": copy.deepcopy(value), + "binding": row["binding"]["binding_id"], + "node_id": row["node_id"], + "created": time.time(), + "deadline": datetime.fromisoformat(value["deadline_at"]).timestamp(), + } + return dict(public) + + def operation(self, fleet, vehicle_id, identifier): + with fleet.lock: + self.prune() + row = fleet.find(vehicle_id) + entry = self.pending.get((vehicle_id, identifier)) + if not entry or entry["binding"] != (row.get("binding") or {}).get("binding_id"): + # A Core restart loses transient delivery; never recreate it + # from browser credentials or imply that a physical write failed. + return { + "operation_id": identifier, + "state": "unknown", + "error": "Результат запроса недоступен. Обновите состояние устройства.", + } + return copy.deepcopy(entry["public"]) + + def heartbeat(self, row, value): + self.prune() + state = value.get("device_enrollment", {"available": False}) + if not isinstance(state, dict) or len(json.dumps(state)) > 32768: + raise ValueError("Invalid device enrollment state") + row["device_enrollment"] = state + acknowledgements = [] + results = value.get("enrollment_results", []) + if not isinstance(results, list) or len(results) > 32: + raise ValueError("Invalid enrollment results") + for result in results: + if not isinstance(result, dict) or len(json.dumps(result)) > 65536: + raise ValueError("Invalid enrollment result") + key = (row["id"], result.get("operation_id")) + entry = self.pending.get(key) + if ( + entry + and entry["binding"] == row["binding"]["binding_id"] + and result.get("state") in {"running", "complete", "error", "unknown"} + ): + entry["payload"] = None # Node's journal now owns the intent. + entry["public"] = { + k: result[k] + for k in ("operation_id", "state", "error", "result") + if k in result + } + if result["state"] != "running": + acknowledgements.append(result["operation_id"]) + elif result.get("state") != "running": + acknowledgements.append(result.get("operation_id")) + commands = [] + for (vehicle_id, _), entry in self.pending.items(): + if vehicle_id != row["id"]: + continue + if entry["binding"] != row["binding"]["binding_id"]: + entry["payload"] = None + entry["public"]["state"] = "unknown" + elif entry["payload"] is not None: + commands.append(entry["payload"]) + return { + "enrollment_commands": commands[:2], + "enrollment_acknowledgements": acknowledgements, + } diff --git a/src/k1link/fleet/registry.py b/src/k1link/fleet/registry.py index 07eb63f..5f7ab01 100644 --- a/src/k1link/fleet/registry.py +++ b/src/k1link/fleet/registry.py @@ -30,6 +30,9 @@ class FleetRegistry: from .events import FleetEvents self.events = FleetEvents() + from .device_enrollment import DeviceEnrollment + + self.device_enrollment = DeviceEnrollment() self.trust = CoreTrust(root) path = root / "fleet.sqlite3" if path.is_symlink(): @@ -378,6 +381,7 @@ class FleetRegistry: return 400, {"error": "Invalid Node inventory"} sensor_state = sensors.validate_inventory(value, node_id) sensor_response = sensors.heartbeat(row, value) + enrollment_response = self.device_enrollment.heartbeat(row, value) host = value.get("host") if ( not isinstance(host, dict) @@ -403,4 +407,4 @@ class FleetRegistry: } row["binding"]["client_pem"] = self.trust.leaf(node_id, key) self.save(row) - return 200, {"ok": True, "client_pem": row["binding"]["client_pem"], **sensor_response} + return 200, {"ok": True, "client_pem": row["binding"]["client_pem"], **sensor_response, **enrollment_response} diff --git a/src/k1link/viewer/node_media.py b/src/k1link/viewer/node_media.py new file mode 100644 index 0000000..da8a828 --- /dev/null +++ b/src/k1link/viewer/node_media.py @@ -0,0 +1,170 @@ +"""Paired signalling, private ICE candidates and bounded live data channels.""" + +import asyncio +import ipaddress +import queue +import time +from contextlib import suppress +from uuid import uuid4 + +import aioice.ice +from aiortc import RTCConfiguration, RTCPeerConnection, RTCSessionDescription + +PRIVATE_NETWORKS = tuple( + ipaddress.ip_network(v) + for v in ("127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10") +) + + +def private(address): + try: + value = ipaddress.ip_address(address) + return value.version == 4 and any(value in network for network in PRIVATE_NETWORKS) + except ValueError: + return False + + +def admit_sdp(sdp): + if not isinstance(sdp, str) or not 1 <= len(sdp) <= 32768: + raise ValueError("Invalid media invitation") + media = [line for line in sdp.splitlines() if line.startswith("m=")] + if not sdp.startswith("v=0") or len(media) != 1 or not media[0].startswith("m=application "): + raise ValueError("A single data-channel media section is required") + for line in sdp.splitlines(): + if line.startswith("a=candidate:"): + parts = line.split() + if ( + len(parts) < 8 + or parts[7] != "host" + or not (private(parts[4]) or parts[4].endswith(".local")) + ): + raise ValueError("Only private host ICE candidates are admitted") + + +class NodeMediaPeers: + def __init__(self, hub, camera): + self.hub, self.camera, self.items = hub, camera, {} + # Process-local adapter in this dedicated worker; no public/STUN/TURN ICE. + original = aioice.ice.get_host_addresses + aioice.ice.get_host_addresses = lambda use_ipv4, use_ipv6: [ + value for value in original(use_ipv4=True, use_ipv6=False) if private(value) + ] + + async def offer(self, parameters): + admit_sdp(parameters.get("sdp")) + if len(self.items) >= 2: + raise ValueError("Close another live viewer") + pc = RTCPeerConnection(RTCConfiguration(iceServers=[])) + identifier = "peer_" + uuid4().hex + entry = {"pc": pc, "seen": time.monotonic(), "tasks": [], "labels": set()} + self.items[identifier] = entry + + @pc.on("datachannel") + def datachannel(channel): + if channel.label not in {"rrd", "camera"} or channel.label in entry["labels"]: + channel.close() + return + entry["labels"].add(channel.label) + + @channel.on("message") + def message(value): + if value == "keepalive": + entry["seen"] = time.monotonic() + + entry["tasks"].append(asyncio.create_task(self.deliver(identifier, channel))) + + @pc.on("connectionstatechange") + async def changed(): + if pc.connectionState in {"closed", "failed"}: + await self.close(identifier) + + try: + await pc.setRemoteDescription( + RTCSessionDescription(sdp=parameters["sdp"], type="offer") + ) + await pc.setLocalDescription(await pc.createAnswer()) + + async def expiry(): + await asyncio.sleep(25) + if pc.connectionState != "connected": + await self.close(identifier) + + entry["tasks"].append(asyncio.create_task(expiry())) + snapshot = self.camera.snapshot() + return { + "peer_id": identifier, + "sdp": pc.localDescription.sdp, + "type": "answer", + "camera_mime": (snapshot.get("delivery") or {}).get("media_type"), + "profile": "live-acquisition", + "transport": "webrtc-rrd-fmp4", + } + except BaseException: + await self.close(identifier) + raise + + async def send(self, channel, payload): + if len(payload) > 8 * 1024 * 1024: + raise RuntimeError("Preview fragment exceeds bound") + for offset in range(0, len(payload), 16384): + deadline = time.monotonic() + 2 + while channel.readyState != "open" or channel.bufferedAmount > 1024 * 1024: + if channel.readyState in {"closed", "closing"} or time.monotonic() > deadline: + raise RuntimeError("Preview consumer unavailable") + await asyncio.sleep(0.01) + channel.send(payload[offset : offset + 16384]) + await asyncio.sleep(0) + + async def deliver(self, identifier, channel): + subscriber = lease = None + try: + entry = self.items[identifier] + if channel.label == "rrd": + subscriber = await asyncio.to_thread(self.hub.subscribe) + else: + generation = self.camera.snapshot().get("generation") + if generation is None: + channel.close() + return + lease = await asyncio.to_thread(self.camera.open_delivery, generation) + while identifier in self.items and time.monotonic() - entry["seen"] < 30: + if subscriber: + payload = await asyncio.to_thread(subscriber.read) + else: + try: + segment = await asyncio.to_thread(lease.segments.get, 0.5) + except queue.Empty: + continue + if segment is None: + break + kind, payload = segment + if kind == "media": + self.camera.mark_streaming(lease) + if payload is None: + break + if payload: + await self.send(channel, payload) + except (Exception, asyncio.CancelledError): + pass + finally: + if subscriber: + subscriber.close() + if lease: + self.camera.release_delivery(lease, client_closed=True) + if channel.label == "camera": + channel.close() + else: + await self.close(identifier) + + async def close(self, identifier): + entry = self.items.pop(identifier, None) + if entry: + for task in entry["tasks"]: + if task is not asyncio.current_task(): + task.cancel() + with suppress(Exception): + await entry["pc"].close() + + async def close_all(self): + for identifier in list(self.items): + await self.close(identifier) diff --git a/src/k1link/viewer/node_rerun.py b/src/k1link/viewer/node_rerun.py new file mode 100644 index 0000000..7d1f56f --- /dev/null +++ b/src/k1link/viewer/node_rerun.py @@ -0,0 +1,144 @@ +"""Bounded live RRD publication for paired Node viewers, without a TCP listener. + +Every viewer receives a fresh native recording including StoreInfo/blueprint. +Latest-value queues discard decoded preview frames before encoding; encoded +RRD bytes are never dropped inside a stream. Slow viewers are closed instead. +""" + +import queue +import threading +from contextlib import suppress +from uuid import uuid4 + +from k1link.viewer.rerun_bridge import RerunBridge + +MAX_ENCODED_CHUNK = 8 * 1024 * 1024 + + +class RrdSubscriber: + def __init__(self, settings_provider): + self.closed = threading.Event() + self.inputs = queue.Queue(maxsize=2) + self.output = queue.Queue(maxsize=2) + self.settings_provider = settings_provider + self.thread = threading.Thread(target=self.run, name="node-rerun-view", daemon=True) + self.thread.start() + + def offer(self, envelope): + if self.closed.is_set(): + return + with suppress(queue.Full): + if self.inputs.full(): + with suppress(queue.Empty): + self.inputs.get_nowait() + self.inputs.put_nowait(envelope) + + def read(self): + if self.closed.is_set(): + return None + try: + return self.output.get(timeout=0.5) + except queue.Empty: + return b"" + + def close(self): + self.closed.set() + + def run(self): + binary = None + bridge = None + + def output(recording): + nonlocal binary + binary = recording.binary_stream() + return "webrtc+rrd://" + str(uuid4()) + + try: + bridge = RerunBridge(settings_provider=self.settings_provider, recording_output=output) + bridge.begin_session() + while not self.closed.is_set(): + payload = binary.read() + if len(payload) > MAX_ENCODED_CHUNK: + break + if payload: + # Bound both bytes and waiting time. The archive/producer + # never waits for this disposable preview subscription. + self.output.put(payload, timeout=0.5) + try: + envelope = self.inputs.get(timeout=0.1) + except queue.Empty: + continue + bridge.process(envelope) + except Exception: + pass + finally: + self.closed.set() + if bridge is not None: + with suppress(Exception): + bridge.close() + binary.read() + + +class NodeRerunBridge(RerunBridge): + def __init__(self, **kwargs): + self.lock = threading.Lock() + self.subscribers = [] + self.latest = {} + + def output(recording): + self.binary = recording.binary_stream() + return "webrtc+rrd://" + str(uuid4()) + + super().__init__(**kwargs, recording_output=output) + self.binary.read() + + def process(self, envelope): + super().process(envelope) + # The primary recording proves native publication and owns metrics. + # It is continuously drained even when no viewer is attached. + self.binary.read() + with self.lock: + self.latest[type(envelope)] = envelope + self.subscribers = [v for v in self.subscribers if not v.closed.is_set()] + for subscriber in self.subscribers: + subscriber.offer(envelope) + + def process_perception(self, frame): + super().process_perception(frame) + self.binary.read() + + def subscribe(self): + with self.lock: + self.subscribers = [v for v in self.subscribers if not v.closed.is_set()] + if self._closed or len(self.subscribers) >= 2: + raise RuntimeError("Live viewer unavailable") + subscriber = RrdSubscriber(self._settings_provider) + for envelope in self.latest.values(): + subscriber.offer(envelope) + self.subscribers.append(subscriber) + return subscriber + + def close(self): + with self.lock: + for subscriber in self.subscribers: + subscriber.close() + self.subscribers.clear() + self.latest.clear() + super().close() + self.binary.read() + + +class NodeRerunHub: + def __init__(self): + self.bridge = None + + def create(self, **kwargs): + bridge = NodeRerunBridge(**kwargs) + self.bridge = bridge + return bridge + + def subscribe(self): + bridge = self.bridge + if bridge is None: + raise RuntimeError("Live acquisition is not active") + return bridge.subscribe() diff --git a/src/k1link/viewer/rerun_bridge.py b/src/k1link/viewer/rerun_bridge.py index 95f1574..62783c5 100644 --- a/src/k1link/viewer/rerun_bridge.py +++ b/src/k1link/viewer/rerun_bridge.py @@ -118,6 +118,7 @@ class RerunBridge: settings_provider: SettingsProvider | None = None, cors_allow_origin: tuple[str, ...] = DEFAULT_CORS_ORIGINS, recording_factory: Callable[[str], rr.RecordingStream] | None = None, + recording_output: Callable[[rr.RecordingStream], str] | None = None, ) -> None: self.metrics = metrics or BridgeMetrics() self._settings_provider = settings_provider or RerunSceneSettings @@ -130,31 +131,29 @@ class RerunBridge: else: recording = recording_factory("nodedc_mission_core_spatial") try: - selected_grpc_port = _select_available_grpc_port(grpc_port) - if selected_grpc_port != grpc_port: - logger.info( - "Mission Core selected a new Rerun port because an earlier " - "viewer still owns the preferred listener", - extra={ - "event_code": "rerun_grpc_port_rotated", - "preferred_port": grpc_port, - "selected_port": selected_grpc_port, - }, - ) blueprint = _blueprint(self._settings) - url = recording.serve_grpc( - grpc_port=selected_grpc_port, - default_blueprint=blueprint, - # This is a reconnect cushion for the live preview, not the source - # of record. Raw MQTT evidence is persisted independently. A large - # late-client backlog can block the native SDK and freeze preview. - server_memory_limit=LIVE_GRPC_BUFFER_LIMIT, - # Rerun transport can replay ActivateStore before StoreInfo when an - # evicted buffer is served newest-first, leaving late viewers on the - # welcome screen. Preserve protocol order within the bounded cache. - newest_first=False, - cors_allow_origin=list(cors_allow_origin), - ) + if recording_output is not None: + # Node's paired WebRTC delivery uses an RRD sink and never opens + # an unauthenticated gRPC listener on an onboard interface. + url = recording_output(recording) + else: + selected_grpc_port = _select_available_grpc_port(grpc_port) + if selected_grpc_port != grpc_port: + logger.info( + "Mission Core selected a new Rerun port because an earlier viewer " + "still owns the preferred listener", + extra={"event_code": "rerun_grpc_port_rotated", "preferred_port": grpc_port, + "selected_port": selected_grpc_port}, + ) + url = recording.serve_grpc( + grpc_port=selected_grpc_port, + default_blueprint=blueprint, + # Bounded reconnect cushion, not the source of record. + server_memory_limit=LIVE_GRPC_BUFFER_LIMIT, + # StoreInfo must precede ActivateStore for late viewers. + newest_first=False, + cors_allow_origin=list(cors_allow_origin), + ) recording.send_blueprint( blueprint, make_active=True, diff --git a/src/k1link/web/fleet_api.py b/src/k1link/web/fleet_api.py index d6b6650..befe47d 100644 --- a/src/k1link/web/fleet_api.py +++ b/src/k1link/web/fleet_api.py @@ -136,3 +136,52 @@ def sensor_operation( return operation(fleet, vehicle_id, operation_id) except PairingError as error: raise HTTPException(404, str(error)) from None + + +@router.get("/{vehicle_id}/devices/enrollment") +def enrollment_state( + vehicle_id: str, response: Response, fleet: Annotated[FleetRegistry, Depends(local_operator)] +): + response.headers["Cache-Control"] = "no-store" + try: + with fleet.lock: + row = fleet.find(vehicle_id) + return { + **row.get("device_enrollment", {"available": False}), + "node_id": row["node_id"], + "name": row["name"], + "fresh": fleet.public(row)["connectivity"] == "online", + } + except PairingError as error: + raise HTTPException(404, str(error)) from None + + +@router.post("/{vehicle_id}/devices/enrollment/operations") +def enrollment_submit( + vehicle_id: str, + body: dict, + response: Response, + fleet: Annotated[FleetRegistry, Depends(local_operator)], +): + response.headers["Cache-Control"] = "no-store" + try: + return fleet.device_enrollment.submit(fleet, vehicle_id, body) + except (PairingError, ValueError, TypeError): + # Validation diagnostics must never echo a supplied credential. + raise HTTPException( + 409, "Запрос подключения не принят. Обновите БК и проверьте параметры сети." + ) from None + + +@router.get("/{vehicle_id}/devices/enrollment/operations/{operation_id}") +def enrollment_operation( + vehicle_id: str, + operation_id: str, + response: Response, + fleet: Annotated[FleetRegistry, Depends(local_operator)], +): + response.headers["Cache-Control"] = "no-store" + try: + return fleet.device_enrollment.operation(fleet, vehicle_id, operation_id) + except PairingError as error: + raise HTTPException(404, str(error)) from None diff --git a/src/k1link/web/runtime_diagnostics.py b/src/k1link/web/runtime_diagnostics.py index f02b9d2..70431f5 100644 --- a/src/k1link/web/runtime_diagnostics.py +++ b/src/k1link/web/runtime_diagnostics.py @@ -18,6 +18,7 @@ _EXTRA_FIELDS: Final = ( "connection_mode", "error_category", "error_code", + "transport_error_code", "reason_code", "failed_phase", "dialogue_stage", @@ -32,6 +33,20 @@ _EXTRA_FIELDS: Final = ( "device_write_confirmed", "ble_att_error_code", "ble_att_error_name", + "resolved_write_mode", + "max_write_without_response_size", + "mtu_size", + "frame_length", + "bridge_frame_length", + "quick_connect_frame_length", + "scan_elapsed_ms", + "scanner_start_ms", + "initial_window_ms", + "first_candidate_ms", + "scan_extended", + "candidate_count", + "likely_k1_candidate_count", + "discovery_generation", "helper_stage", "helper_elapsed_ms", "status_reconciliation", @@ -102,6 +117,26 @@ class ScannerDiagnosticJsonFormatter(logging.Formatter): value = getattr(record, field, None) if value is not None and isinstance(value, (str, int, bool)): document[field] = value + if record.exc_info is not None: + exception_type, _exception, traceback = record.exc_info + if exception_type is not None: + document["exception_type"] = exception_type.__name__[:128] + if traceback is not None: + while traceback.tb_next is not None: + traceback = traceback.tb_next + code = traceback.tb_frame.f_code + # Preserve the failure location, never exception text, locals, + # source lines or absolute paths that may contain private data. + document["exception_site"] = ( + f"{Path(code.co_filename).name}:{traceback.tb_lineno}:{code.co_name}" + )[:256] + properties = getattr(record, "write_characteristic_properties", None) + if isinstance(properties, (list, tuple)) and all( + isinstance(value, str) + and value in {"read", "write", "write-without-response", "notify", "indicate"} + for value in properties + ): + document["write_characteristic_properties"] = sorted(set(properties)) return json.dumps(document, ensure_ascii=False, separators=(",", ":")) diff --git a/tests/fleet/test_device_enrollment.py b/tests/fleet/test_device_enrollment.py new file mode 100644 index 0000000..38e536e --- /dev/null +++ b/tests/fleet/test_device_enrollment.py @@ -0,0 +1,111 @@ +import copy +import json +import secrets +import threading +from datetime import UTC, datetime, timedelta + +import pytest + +from k1link.fleet.device_enrollment import DeviceEnrollment, validate +from k1link.fleet.trust import PairingError + + +class Fleet: + def __init__(self): + self.lock = threading.RLock() + self.row = { + "id": "vehicle", + "node_id": "node-1", + "binding": {"binding_id": "binding-1"}, + "device_enrollment": {"available": True, "runtime_id": "runtime-1"}, + } + + def find(self, _identifier): + return self.row + + def public(self, row): + return {"connectivity": "online"} + + +def command(): + # Synthetic value constructed at runtime; never a real WLAN credential. + return { + "operation_id": "op_" + "a" * 32, + "node_id": "node-1", + "runtime_id": "runtime-1", + "action": "connect", + "deadline_at": (datetime.now(UTC) + timedelta(seconds=60)).isoformat(), + "parameters": { + "device_id": "AA:BB:CC:DD:EE:FF", + "discovery_generation": 1, + "mode_revision": 0, + "ssid": "test-net", + "password": secrets.token_urlsafe(20), + }, + } + + +def test_password_only_lives_in_pending_delivery_and_never_in_fleet_or_result(): + fleet, bus, request = Fleet(), DeviceEnrollment(), command() + secret = request["parameters"]["password"] + public = bus.submit(fleet, "vehicle", request) + assert secret not in json.dumps(public) + assert secret not in json.dumps(fleet.row) + envelope = bus.heartbeat(fleet.row, {}) + assert envelope["enrollment_commands"][0]["parameters"]["password"] == secret + bus.heartbeat( + fleet.row, + {"enrollment_results": [{"operation_id": request["operation_id"], "state": "running"}]}, + ) + assert secret not in json.dumps(list(bus.pending.values())) + assert not bus.heartbeat(fleet.row, {})["enrollment_commands"] + + +def test_unknown_after_core_restart_does_not_recreate_command(): + fleet, bus, request = Fleet(), DeviceEnrollment(), command() + bus.submit(fleet, "vehicle", request) + restarted = DeviceEnrollment() + assert restarted.operation(fleet, "vehicle", request["operation_id"])["state"] == "unknown" + assert not restarted.heartbeat(fleet.row, {})["enrollment_commands"] + + +def test_fences_board_runtime_and_deadline_and_forbids_host_actions(): + for change in ( + {"node_id": "node-2"}, + {"runtime_id": "runtime-2"}, + {"action": "quick-connect"}, + {"deadline_at": datetime.now(UTC).isoformat()}, + ): + fleet, bus, request = Fleet(), DeviceEnrollment(), command() + request.update(change) + with pytest.raises(PairingError): + bus.submit(fleet, "vehicle", request) + request = command() + request["parameters"]["allow_host_wifi_switch"] = True + with pytest.raises(PairingError): + validate(request) + + +def test_rebinding_drops_delivery_and_old_results(): + fleet, bus, request = Fleet(), DeviceEnrollment(), command() + bus.submit(fleet, "vehicle", request) + fleet.row["binding"]["binding_id"] = "binding-2" + assert not bus.heartbeat(fleet.row, {})["enrollment_commands"] + assert bus.operation(fleet, "vehicle", request["operation_id"])["state"] == "unknown" + + +def test_expiry_drops_secret_without_automatic_retry(): + fleet, bus, request = Fleet(), DeviceEnrollment(), command() + bus.submit(fleet, "vehicle", request) + bus.pending[("vehicle", request["operation_id"])]["deadline"] = 0 + assert bus.operation(fleet, "vehicle", request["operation_id"])["state"] == "unknown" + assert bus.pending[("vehicle", request["operation_id"])]["payload"] is None + + +def test_duplicate_id_cannot_replace_original_secret(): + fleet, bus, request = Fleet(), DeviceEnrollment(), command() + bus.submit(fleet, "vehicle", request) + duplicate = copy.deepcopy(request) + duplicate["parameters"]["password"] += "changed" + bus.submit(fleet, "vehicle", duplicate) + assert bus.pending[("vehicle", request["operation_id"])]["payload"] == request diff --git a/tests/test_ble_scanner.py b/tests/test_ble_scanner.py index 4d0c248..8fa1c5b 100644 --- a/tests/test_ble_scanner.py +++ b/tests/test_ble_scanner.py @@ -1,5 +1,5 @@ import asyncio -from collections.abc import Iterator +from collections.abc import Callable, Iterator from pathlib import Path from typing import Literal @@ -36,6 +36,117 @@ from k1link.device_plugins.xgrids_k1.ble.scanner import ( RecoveryOperationKind = Literal["status-read", "wifi-provision", "ap-enable"] +def install_advertisement_source( + monkeypatch: MonkeyPatch, + observations: list[tuple[float, BLEDevice, str | None]], +) -> dict[str, int]: + """A single native scanner that delivers advertisements while it is open.""" + lifecycle = {"started": 0, "stopped": 0} + + class FakeScanner: + def __init__( + self, detection_callback: Callable[[BLEDevice, AdvertisementData], None] + ) -> None: + self.callback = detection_callback + self.scheduled: list[asyncio.TimerHandle] = [] + + async def __aenter__(self) -> "FakeScanner": + lifecycle["started"] += 1 + for delay, device, name in observations: + advertisement = AdvertisementData( + local_name=name, manufacturer_data={}, service_data={}, + service_uuids=[], tx_power=None, rssi=-45, platform_data=(), + ) + self.scheduled.append(asyncio.get_running_loop().call_later( + delay, self.callback, device, advertisement + )) + return self + + async def __aexit__(self, *_args: object) -> None: + lifecycle["stopped"] += 1 + for timer in self.scheduled: + timer.cancel() + + monkeypatch.setattr(scanner_module, "BleakScanner", FakeScanner) + monkeypatch.setattr(scanner_module, "BLE_SCAN_INITIAL_WINDOW_SECONDS", 0.02) + return lifecycle + + +def test_discovery_keeps_one_scanner_open_for_a_late_k1_name(monkeypatch: MonkeyPatch) -> None: + device = BLEDevice("LATE-K1", None, details=object()) + lifecycle = install_advertisement_source(monkeypatch, [ + (0, device, None), (0.05, device, "XGR-LATE"), + ]) + + async def scenario() -> None: + result = await scan(0.4) + timing = result["discovery_timing"] + assert timing["scan_extended"] is True + assert timing["first_candidate_ms"] >= timing["initial_window_ms"] + assert timing["scan_elapsed_ms"] < 400 + assert len(result["devices"]) == 1 + assert result["devices"][0]["k1_name_candidate"] is True + assert discovered_device(device.address) is device + + asyncio.run(scenario()) + assert lifecycle == {"started": 1, "stopped": 1} + + +def test_discovery_finishes_after_initial_window_when_k1_is_visible( + monkeypatch: MonkeyPatch, +) -> None: + device = BLEDevice("EARLY-K1", "XGR-EARLY", details=object()) + lifecycle = install_advertisement_source(monkeypatch, [(0, device, "XGR-EARLY")]) + result = asyncio.run(scan(0.4)) + timing = result["discovery_timing"] + assert timing["scan_extended"] is False + assert timing["first_candidate_ms"] < timing["initial_window_ms"] + assert 20 <= timing["scan_elapsed_ms"] < 400 + assert lifecycle == {"started": 1, "stopped": 1} + + +def test_discovery_without_k1_stops_at_deadline_and_does_not_reuse_old_results( + monkeypatch: MonkeyPatch, +) -> None: + device = BLEDevice("OTHER-DEVICE", "Headphones", details=object()) + lifecycle = install_advertisement_source(monkeypatch, [(0, device, "Headphones")]) + + async def scenario() -> None: + result = await scan(0.06) + timing = result["discovery_timing"] + assert timing["scan_extended"] is True + assert "first_candidate_ms" not in timing + assert timing["scan_elapsed_ms"] >= 60 + assert result["devices"][0]["k1_name_candidate"] is False + install_advertisement_source(monkeypatch, []) + empty = await scan(0.01) + assert empty["devices"] == [] + assert empty["discovery_timing"]["scan_extended"] is False + assert discovered_device(device.address) is None + + asyncio.run(scenario()) + assert lifecycle == {"started": 1, "stopped": 1} + + +def test_cancelled_discovery_closes_scanner_without_publishing_partial_handles( + monkeypatch: MonkeyPatch, +) -> None: + device = BLEDevice("PARTIAL", "Headphones", details=object()) + lifecycle = install_advertisement_source(monkeypatch, [(0, device, "Headphones")]) + + async def scenario() -> None: + task = asyncio.create_task(scan(1)) + await asyncio.sleep(0.04) + assert lifecycle["started"] == 1 + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + assert discovered_device(device.address) is None + + asyncio.run(scenario()) + assert lifecycle == {"started": 1, "stopped": 1} + + async def _retrieve_connected_inside_ble_lease( macos_uuid: str, *, @@ -175,7 +286,7 @@ def test_scan_retains_the_live_corebluetooth_handle( return {"LIVE-UUID": (device, advertisement)} monkeypatch.setattr( - "k1link.device_plugins.xgrids_k1.ble.scanner.BleakScanner.discover", + "k1link.device_plugins.xgrids_k1.ble.scanner._discover_k1_advertisements", fake_discover, ) @@ -209,7 +320,7 @@ def test_scan_start_invalidates_previous_lease_and_failure_leaves_it_empty( ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: return {old_device.address: (old_device, old_advertisement)} - monkeypatch.setattr(scanner_module.BleakScanner, "discover", initial_discover) + monkeypatch.setattr(scanner_module, "_discover_k1_advertisements", initial_discover) await scan(1.0) assert discovered_device(old_device.address) is old_device @@ -221,7 +332,7 @@ def test_scan_start_invalidates_previous_lease_and_failure_leaves_it_empty( await release_failing_scan.wait() raise RuntimeError("synthetic BLE scan failure") - monkeypatch.setattr(scanner_module.BleakScanner, "discover", failing_discover) + monkeypatch.setattr(scanner_module, "_discover_k1_advertisements", failing_discover) scan_task = asyncio.create_task(scan(1.0)) await failing_scan_started.wait() @@ -263,7 +374,7 @@ def test_process_arbiter_rejects_overlapping_scan(monkeypatch: MonkeyPatch) -> N await release_older_scan.wait() return {older_device.address: (older_device, older_advertisement)} - monkeypatch.setattr(scanner_module.BleakScanner, "discover", blocked_discover) + monkeypatch.setattr(scanner_module, "_discover_k1_advertisements", blocked_discover) older_task = asyncio.create_task(scan(1.0)) await older_scan_started.wait() @@ -377,7 +488,7 @@ def test_connected_handle_is_session_scoped_and_survives_wall_clock_age( async def fake_discover(**_kwargs: object) -> dict[str, tuple[BLEDevice, AdvertisementData]]: return discoveries.pop(0) - monkeypatch.setattr(scanner_module.BleakScanner, "discover", fake_discover) + monkeypatch.setattr(scanner_module, "_discover_k1_advertisements", fake_discover) monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0]) monkeypatch.setattr( scanner_module, @@ -487,7 +598,7 @@ def test_scan_and_retained_handle_survive_macos_sleep_until_explicit_invalidatio ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: return {device.address: (device, advertisement)} - monkeypatch.setattr(scanner_module.BleakScanner, "discover", fake_discover) + monkeypatch.setattr(scanner_module, "_discover_k1_advertisements", fake_discover) monkeypatch.setattr(scanner_module, "monotonic", lambda: monotonic_clock[0]) monkeypatch.setattr( scanner_module, @@ -1340,7 +1451,7 @@ def test_later_fresh_scan_does_not_replace_retained_corebluetooth_object( async def fake_discover(**_kwargs: object) -> dict[str, tuple[BLEDevice, AdvertisementData]]: return discoveries.pop(0) - monkeypatch.setattr(scanner_module.BleakScanner, "discover", fake_discover) + monkeypatch.setattr(scanner_module, "_discover_k1_advertisements", fake_discover) async def scenario() -> None: await scan(1.0) diff --git a/tests/test_k1_wifi_failure.py b/tests/test_k1_wifi_failure.py new file mode 100644 index 0000000..f64d308 --- /dev/null +++ b/tests/test_k1_wifi_failure.py @@ -0,0 +1,72 @@ +from copy import deepcopy + +import pytest + +from k1link.device_plugins.xgrids_k1.wifi_failure import reviewed_station_failure_code + + +def station_error(code: int = 4) -> dict[str, object]: + return { + "code": "BleakGATTProtocolError", + "operation_stage": "gatt-write", + "device_write_attempted": True, + "device_write_confirmed": False, + "resolved_write_mode": "with_response", + "frame_length": 99, + "ble_att_error_code": code, + "ble_att_error_name": "INVALID_PDU", + "safe_to_retry": False, + "side_effect_status": "unknown", + } + + +@pytest.mark.parametrize("mode", ["bridge", "direct-connect"]) +@pytest.mark.parametrize( + "code,reason", [(4, "k1-wifi-network-not-found"), (6, "k1-wifi-credentials-required")] +) +def test_reviewed_station_reply_retains_raw_facts_and_retry_fence( + mode: str, code: int, reason: str +) -> None: + error = station_error(code) + before = deepcopy(error) + assert ( + reviewed_station_failure_code(error, firmware_version="3.0.2", connection_mode=mode) + == reason + ) + assert error == before + + +@pytest.mark.parametrize( + "field,value", + [ + ("code", "RuntimeError"), + ("operation_stage", "baseline-read"), + ("device_write_attempted", False), + ("device_write_confirmed", True), + ("resolved_write_mode", "without_response"), + ("frame_length", 100), + ("ble_att_error_code", 14), + ("ble_att_error_code", "4"), + ("ble_att_error_code", {}), + ], +) +def test_unrelated_transport_failures_are_not_wifi_diagnoses(field: str, value: object) -> None: + error = {**station_error(), field: value} + assert ( + reviewed_station_failure_code(error, firmware_version="3.0.2", connection_mode="bridge") + is None + ) + + +@pytest.mark.parametrize( + "firmware,mode", [("3.0.1", "bridge"), ("unknown", "bridge"), ("3.0.2", "quick-connect")] +) +def test_other_firmware_and_quick_connect_do_not_inherit_station_codes( + firmware: str, mode: str +) -> None: + assert ( + reviewed_station_failure_code( + station_error(), firmware_version=firmware, connection_mode=mode + ) + is None + ) diff --git a/tests/test_node_k1_bridge.py b/tests/test_node_k1_bridge.py new file mode 100644 index 0000000..680fe8a --- /dev/null +++ b/tests/test_node_k1_bridge.py @@ -0,0 +1,204 @@ +import asyncio +import copy +import json +import secrets +from datetime import UTC, datetime, timedelta +from pathlib import Path + +import pytest +from missioncore_plugin_sdk.v0alpha2.session import DeviceSessionSnapshot + +from k1link.device_plugins.xgrids_k1.linux_host import nm_fields, route_fields +from k1link.device_plugins.xgrids_k1.node_bridge import NodeBridge +from k1link.device_plugins.xgrids_k1.node_sensor import NodeK1Sensor, project_sensor +from k1link.viewer.node_rerun import NodeRerunHub + + +def state(): + return { + "snapshot_runtime_id": "runtime-one", + "snapshot_revision": 1, + "devices": [ + {"device_id": "AA:BB:CC:DD:EE:FF", "name": "K1-test", "likely_k1": True}, + {"device_id": "unrelated", "name": "Other", "likely_k1": False}, + ], + "ble_discovery_generation": 1, + "desired_connection_mode_revision": 0, + "active_connection_mode": "bridge", + "connection_lifecycle": {"connection_ready": True, "ready_to_start": True}, + "selected_device_id": "AA:BB:CC:DD:EE:FF", + "source_mode": "idle", + "device_session": { + "device_id": "synthetic-k1", + "device_session_id": "session-test", + "opened_at": datetime.now(UTC).isoformat(), + }, + "application_control_session": { + "session_generation": 1, + "state_revision": 1, + "state": "connection-ready", + }, + } + + +class Facade: + def __init__(self): + self.current = state() + self.actions = [] + + async def invoke(self, request): + self.actions.append((request.action_id, copy.deepcopy(request.parameters))) + if request.action_id == "application-control.workspace.enter": + self.current["application_control_session"]["state"] = "workspace-ready" + elif request.action_id == "acquisition.prepare": + self.current["acquisition"] = { + "acquisition_id": "acquisition-test", + "state": "prepared", + "state_revision": 1, + } + self.current["application_control_session"]["state"] = "project-ready" + elif request.action_id == "acquisition.start": + self.current["application_control_session"]["state"] = "initializing" + self.current["acquisition"]["state"] = "running" + self.current["source_mode"] = "live" + return copy.deepcopy(self.current) + + +def bridge(): + result = NodeBridge(Path.cwd(), service=object()) + result.facade = Facade() + return result + + +def test_node_bridge_forces_reviewed_bridge_and_clears_input_secret(): + async def run(): + device = bridge() + command = { + "operation_id": "op_" + "a" * 32, + "runtime_id": "runtime-one", + "deadline_at": (datetime.now(UTC) + timedelta(seconds=60)).isoformat(), + "action": "connect", + "parameters": { + "device_id": "AA:BB:CC:DD:EE:FF", + "discovery_generation": 1, + "mode_revision": 0, + "ssid": "test-network", + "password": secrets.token_urlsafe(24), + "allow_host_wifi_switch": True, + }, + } + secret = command["parameters"]["password"] + output = await device.execute(command) + payload = next(v for a, v in device.facade.actions if a == "network.provision") + assert payload["connection_mode"] == "bridge" + assert payload["allow_host_wifi_switch"] is False + assert payload["password"] == secret + assert "password" not in command["parameters"] + assert secret not in json.dumps(output) + assert [v["id"] for v in output["candidates"]] == ["AA:BB:CC:DD:EE:FF"] + + asyncio.run(run()) + + +def test_stale_discovery_prevents_provisioning(): + async def run(): + device = bridge() + with pytest.raises(ValueError): + await device.execute( + { + "operation_id": "op_" + "a" * 32, + "runtime_id": "runtime-one", + "deadline_at": (datetime.now(UTC) + timedelta(seconds=60)).isoformat(), + "action": "connect", + "parameters": { + "device_id": "AA:BB:CC:DD:EE:FF", + "discovery_generation": 0, + "mode_revision": 0, + }, + } + ) + assert all(action != "network.provision" for action, _ in device.facade.actions) + + asyncio.run(run()) + + +def test_sensor_projection_binds_native_sdk_to_board(): + value = project_sensor(state(), "node-test") + snapshot = DeviceSessionSnapshot.model_validate(value["snapshot"]) + assert snapshot.context.execution.node_id == "node-test" + assert snapshot.context.device.device_id == value["id"] + assert value["kind"] == "k1" + + +def test_one_start_intent_preserves_canonical_enter_prepare_start_sequence(): + async def run(): + device = bridge() + sensor = NodeK1Sensor(device, None) + item = project_sensor(state(), "node-test") + command = { + "operation_id": "op_" + "a" * 32, + "action_id": "start", + "session": {"device_id": item["id"], "session_id": "session-test"}, + "deadline_at": (datetime.now(UTC) + timedelta(seconds=60)).isoformat(), + "parameters": { + "operator_confirmed": True, + "control_generation": 1, + "acquisition_id": None, + }, + } + result = await sensor.execute(command, "node-test") + actions = [a for a, _ in device.facade.actions if a != "state.read"] + assert actions == [ + "application-control.workspace.enter", + "acquisition.prepare", + "acquisition.start", + ] + assert result["snapshot"]["acquisition"] == "streaming" + command["parameters"]["control_generation"] = 2 + with pytest.raises(ValueError): + await sensor.execute(command, "node-test") + assert len([a for a, _ in device.facade.actions if a == "acquisition.start"]) == 1 + + asyncio.run(run()) + + +def test_networkmanager_ssids_are_not_split_at_escaped_colons(): + assert nm_fields(r"field\:network\\name:88:WPA2") == ["field:network\\name", "88", "WPA2"] + + +def test_linux_kernel_route_is_matched_route_not_resolved_host(monkeypatch): + from k1link.device_plugins.xgrids_k1 import linux_host + from k1link.device_plugins.xgrids_k1.facade import _classify_host_route + + calls = [] + + def run(args): + calls.append(args) + return '[{"dst":"default","dev":"wlp2s0","gateway":"192.168.1.1"}]' + + monkeypatch.setattr(linux_host, "_run", run) + result = route_fields("192.168.2.7") + assert calls[0] == ["ip", "-j", "route", "get", "192.168.2.7", "fibmatch"] + assert _classify_host_route(result["interface"], result["destination"])[0] == "default" + assert _classify_host_route("wlp2s0", "192.168.1.0/24")[0] == "direct" + assert _classify_host_route("tailscale0", "192.168.1.7")[0] == "tunnel" + + +def test_native_node_rrd_opens_no_grpc_listener(monkeypatch): + from k1link.viewer import rerun_bridge + + def forbidden(*_args, **_kwargs): + raise AssertionError("Node must not expose a gRPC listener") + + monkeypatch.setattr(rerun_bridge, "_select_available_grpc_port", forbidden) + hub = NodeRerunHub() + publisher = hub.create() + publisher.begin_session() + subscriber = hub.subscribe() + try: + data = subscriber.read() + assert data[:4] == b"RRF2" + finally: + subscriber.close() + subscriber.thread.join(6) + publisher.close() diff --git a/tests/test_node_media.py b/tests/test_node_media.py new file mode 100644 index 0000000..b125df8 --- /dev/null +++ b/tests/test_node_media.py @@ -0,0 +1,102 @@ +import asyncio +import queue + +import pytest +from aiortc import RTCConfiguration, RTCPeerConnection, RTCSessionDescription + +from k1link.viewer.node_media import NodeMediaPeers, admit_sdp + +SDP_HEADER = "v=0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n" + + +@pytest.mark.parametrize( + "address,kind", [("8.8.8.8", "host"), ("192.168.1.2", "relay"), ("::1", "host")] +) +def test_media_rejects_non_private_or_relay_candidates(address, kind): + with pytest.raises(ValueError): + admit_sdp(SDP_HEADER + f"a=candidate:1 1 udp 1 {address} 12345 typ {kind}\r\n") + + +def test_media_accepts_paired_lan_and_tailnet_candidates(): + for address in ("192.168.1.2", "100.80.6.113", "peer.local"): + admit_sdp(SDP_HEADER + f"a=candidate:1 1 udp 1 {address} 12345 typ host\r\n") + + +def test_failed_media_offer_retires_peer_without_camera_channel(monkeypatch): + async def reject(*_): + raise ValueError("Invalid remote description") + + monkeypatch.setattr(RTCPeerConnection, "setRemoteDescription", reject) + + async def run(): + class Camera: + def snapshot(self): + return {} + + peers = NodeMediaPeers(None, Camera()) + try: + with pytest.raises(ValueError): + await peers.offer({"sdp": SDP_HEADER}) + assert peers.items == {} + finally: + await peers.close_all() + + asyncio.run(run()) + + +def test_native_webrtc_roundtrip_and_missing_camera_preserve_rrd(monkeypatch): + """One bounded loopback peer, no STUN/TURN, device or external network.""" + import aioice.ice + + class Subscription: + def __init__(self): + self.output = queue.Queue() + self.output.put(b"RRF2-transport-fixture") + + def read(self): + try: + return self.output.get(timeout=0.1) + except queue.Empty: + return b"" + + def close(self): + pass + + class Hub: + def subscribe(self): + return Subscription() + + class Camera: + def snapshot(self): + return {"generation": None} + + async def run(): + peers = NodeMediaPeers(Hub(), Camera()) + monkeypatch.setattr(aioice.ice, "get_host_addresses", lambda **_: ["127.0.0.1"]) + client = RTCPeerConnection(RTCConfiguration(iceServers=[])) + channel = client.createDataChannel("rrd", ordered=True) + client.createDataChannel("camera", ordered=True) + received = asyncio.Event() + payloads = [] + + @channel.on("message") + def message(data): + payloads.append(data) + received.set() + + try: + await client.setLocalDescription(await client.createOffer()) + answer = await peers.offer({"sdp": client.localDescription.sdp}) + await client.setRemoteDescription( + RTCSessionDescription(sdp=answer["sdp"], type="answer") + ) + await asyncio.wait_for(received.wait(), timeout=8) + assert payloads == [b"RRF2-transport-fixture"] + assert answer["peer_id"] in peers.items + assert channel.readyState == "open" + finally: + await client.close() + await peers.close_all() + assert not peers.items + + asyncio.run(run()) diff --git a/tests/test_viewer_diagnostics_api.py b/tests/test_viewer_diagnostics_api.py index 8ba6fd9..1ea6de0 100644 --- a/tests/test_viewer_diagnostics_api.py +++ b/tests/test_viewer_diagnostics_api.py @@ -42,6 +42,29 @@ def _request(*, ui_build_id: str | None = None) -> Request: return Request({"type": "http", "method": "GET", "path": "/", "headers": headers}) +def test_scanner_exception_keeps_location_without_private_message_or_locals( + tmp_path: Path, +) -> None: + target = configure_scanner_diagnostics(tmp_path / "logs") + logger = logging.getLogger(f"{SCANNER_LOGGER_NAME}.test") + private_detail = "synthetic-private-runtime-detail" + try: + raise ValueError(private_detail) + except ValueError: + logger.exception("Camera activation failed") + for handler in logging.getLogger(SCANNER_LOGGER_NAME).handlers: + handler.flush() + serialized = target.read_text(encoding="utf-8").splitlines()[-1] + document = json.loads(serialized) + assert document["exception_type"] == "ValueError" + assert document["exception_site"].startswith("test_viewer_diagnostics_api.py:") + assert document["exception_site"].endswith( + ":test_scanner_exception_keeps_location_without_private_message_or_locals" + ) + assert private_detail not in serialized + assert str(Path(__file__).parent) not in serialized + + def test_private_scanner_diagnostics_are_durable_structured_and_bounded( tmp_path: Path, ) -> None: @@ -67,6 +90,19 @@ def test_private_scanner_diagnostics_are_durable_structured_and_bounded( "device_write_confirmed": False, "ble_att_error_code": 4, "ble_att_error_name": "INVALID_PDU", + "resolved_write_mode": "with_response", + "max_write_without_response_size": 253, + "mtu_size": 256, + "frame_length": 99, + "write_characteristic_properties": ["read", "write"], + "scan_elapsed_ms": 7300, + "scanner_start_ms": 31, + "initial_window_ms": 6000, + "first_candidate_ms": 7240, + "scan_extended": True, + "candidate_count": 4, + "likely_k1_candidate_count": 1, + "discovery_generation": 2, "helper_stage": "compile", "helper_elapsed_ms": 34720, "camera_source_id": "sensor.camera.right", @@ -126,6 +162,19 @@ def test_private_scanner_diagnostics_are_durable_structured_and_bounded( assert document["device_write_confirmed"] is False assert document["ble_att_error_code"] == 4 assert document["ble_att_error_name"] == "INVALID_PDU" + assert document["resolved_write_mode"] == "with_response" + assert document["max_write_without_response_size"] == 253 + assert document["mtu_size"] == 256 + assert document["frame_length"] == 99 + assert document["write_characteristic_properties"] == ["read", "write"] + assert document["scan_elapsed_ms"] == 7300 + assert document["scanner_start_ms"] == 31 + assert document["initial_window_ms"] == 6000 + assert document["first_candidate_ms"] == 7240 + assert document["scan_extended"] is True + assert document["candidate_count"] == 4 + assert document["likely_k1_candidate_count"] == 1 + assert document["discovery_generation"] == 2 assert document["helper_stage"] == "compile" assert document["helper_elapsed_ms"] == 34720 assert document["camera_source_id"] == "sensor.camera.right" diff --git a/tests/test_xgrids_acquisition_lifecycle.py b/tests/test_xgrids_acquisition_lifecycle.py index 02425a3..2222a38 100644 --- a/tests/test_xgrids_acquisition_lifecycle.py +++ b/tests/test_xgrids_acquisition_lifecycle.py @@ -5256,11 +5256,14 @@ def test_ble_scan_operation_id_is_exactly_once_and_request_bound( if on_admitted is not None: on_admitted() transport_calls += 1 - return _ble_scan_result("exactly-once-device") + return { + **_ble_scan_result("exactly-once-device"), + "discovery_timing": {"scan_elapsed_ms": 7100, "scan_extended": True}, + } monkeypatch.setattr(facade_module, "scan", successful_scan) operation_id = "op-00000000-0000-4000-8000-000000000001" - request = BleScanRequest(duration_seconds=6.0, operation_id=operation_id) + request = BleScanRequest(operation_id=operation_id) first = asyncio.run(service.scan_ble(request)) repeated = asyncio.run(service.scan_ble(request)) @@ -5271,8 +5274,10 @@ def test_ble_scan_operation_id_is_exactly_once_and_request_bound( assert first["last_operation"]["result"] == { "candidate_count": 1, "likely_k1_candidate_count": 1, - "duration_seconds": 6.0, + "duration_seconds": 20.0, "discovery_generation": 1, + "scan_elapsed_ms": 7100, + "scan_extended": True, } assert repeated["last_operation"]["operation_id"] == operation_id with pytest.raises(ValueError, match="different request"): @@ -21315,6 +21320,61 @@ def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host( assert "lab-network" not in caplog.text +@pytest.mark.parametrize( + ("att_code", "public_code"), + [(4, "k1-wifi-network-not-found"), (6, "k1-wifi-credentials-required")], +) +def test_station_reply_preserves_ambiguity_and_explains_wifi_failure( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + att_code: int, + public_code: str, +) -> None: + from bleak.exc import BleakGATTProtocolError + + service, _ = service_with_fake_runtime(tmp_path) + _set_scanned_devices(service, [{"device_id": "k1-a"}]) + calls = 0 + + async def rejected_write( + *_: object, on_write_dispatch: Any = None, **__: object, + ) -> dict[str, Any]: + nonlocal calls + calls += 1 + on_write_dispatch(_wifi_status_read(None, device_id="k1-a")["status"], "with_response") + exc = BleakGATTProtocolError(att_code) + exc.operation_stage = "gatt-write" # type: ignore[attr-defined] + exc.device_write_attempted = True # type: ignore[attr-defined] + exc.device_write_confirmed = False # type: ignore[attr-defined] + exc.att_error_code = att_code # type: ignore[attr-defined] + exc.att_error_name = exc.code.name # type: ignore[attr-defined] + exc.resolved_write_mode = "with_response" # type: ignore[attr-defined] + exc.frame_length = 99 # type: ignore[attr-defined] + raise exc + + monkeypatch.setattr(facade_module, "provision_wifi_once", rejected_write) + with pytest.raises(BleakGATTProtocolError): + asyncio.run(service.connect(_connect_request( + device_id="k1-a", ssid="lab-network", + password=SecretStr(PRIMARY_TEST_CREDENTIAL), + compatibility_attestation=ATTESTATION, + idempotency_key="explicit-station-rejection", + ))) + state = service.state() + operation = next(item for item in state["operations"] if item["action"] == "network.provision") + error = operation["error"] + assert calls == 1 + assert operation["status"] == "failed" + assert error["code"] == public_code + assert error["transport_error_code"] == "BleakGATTProtocolError" + assert error["ble_att_error_code"] == att_code + assert error["safe_to_retry"] is False + assert error["device_write_confirmed"] is False + assert error["side_effect_status"] == "unknown" + assert state["connection_attempt"]["public_error_code"] == public_code + assert state["network_mutation_ledger"]["status"] == "unresolved" + + def test_ambiguous_ble_write_is_audit_only_for_next_explicit_intent( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, diff --git a/tests/test_xgrids_ble_runtime_arbiter.py b/tests/test_xgrids_ble_runtime_arbiter.py index 8660a9f..9c67158 100644 --- a/tests/test_xgrids_ble_runtime_arbiter.py +++ b/tests/test_xgrids_ble_runtime_arbiter.py @@ -181,7 +181,7 @@ def test_scan_lease_rejects_wifi_entrypoints_process_wide( resolution_attempted = True raise AssertionError("busy contender must not touch CoreBluetooth") - monkeypatch.setattr(scanner_module.BleakScanner, "discover", blocked_discover) + monkeypatch.setattr(scanner_module, "_discover_k1_advertisements", blocked_discover) monkeypatch.setattr( wifi_module.BleakScanner, "find_device_by_address", diff --git a/tests/test_xgrids_camera_gateway.py b/tests/test_xgrids_camera_gateway.py index 7ed0055..77c89a4 100644 --- a/tests/test_xgrids_camera_gateway.py +++ b/tests/test_xgrids_camera_gateway.py @@ -299,6 +299,99 @@ def test_camera_selection_is_exclusive_and_hides_device_transport( gateway.close() +@pytest.mark.parametrize("entrypoint", ["acquisition", "selected-preview"]) +def test_camera_records_in_configured_evidence_outside_source_checkout( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, entrypoint: str, +) -> None: + checkout = tmp_path / "checkout" + checkout.mkdir() + evidence = tmp_path / "durable" / "sessions" + session = evidence / "synthetic-camera-session" + session.mkdir(parents=True) + monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path))) + gateway = XgridsK1CameraGateway(checkout, XGRIDS_K1_PLUGIN_ID, evidence_root=evidence) + try: + if entrypoint == "acquisition": + gateway.activate_recording_producer( + "sensor.camera.right", "192.168.1.20", session, + pre_prepare_fence=lambda reserve: reserve(), + commit_fence=lambda commit: commit(), + ) + else: + gateway.select("sensor.camera.right", "192.168.1.20") + gateway.start_recording(session) + _wait_until(lambda: gateway.snapshot()["recording"]["media_ready"] is True) + assert gateway.snapshot()["recording"]["session"] == session.name + assert not list(checkout.rglob("*.m4s")) + finally: + gateway.close() + assert any(session.rglob("*.m4s")) + + +@pytest.mark.parametrize("entrypoint", ["acquisition", "selected-preview"]) +@pytest.mark.parametrize("escape", ["sibling", "symlink"]) +def test_camera_rejects_paths_outside_configured_evidence_before_admission( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, entrypoint: str, escape: str, +) -> None: + checkout = tmp_path / "checkout" + checkout.mkdir() + evidence = tmp_path / "evidence" + evidence.mkdir() + outside = checkout / "not-evidence" + outside.mkdir() + session = outside + if escape == "symlink": + session = evidence / "escaped-session" + session.symlink_to(outside, target_is_directory=True) + gateway = XgridsK1CameraGateway(checkout, XGRIDS_K1_PLUGIN_ID, evidence_root=evidence) + + def forbidden(*_args: object, **_kwargs: object) -> bool: + raise AssertionError("An escaped path must not reach authority or FFmpeg") + + monkeypatch.setattr(camera_module.subprocess, "Popen", forbidden) + try: + with pytest.raises(ValueError, match="configured evidence root"): + if entrypoint == "acquisition": + gateway.activate_recording_producer( + "sensor.camera.right", "192.168.1.20", session, + pre_prepare_fence=forbidden, commit_fence=forbidden, + ) + else: + gateway.start_recording(session) + assert gateway.snapshot()["recording"]["active"] is False + finally: + gateway.close() + + +def test_service_camera_uses_the_same_external_evidence_root_as_acquisition( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + checkout = tmp_path / "checkout" + checkout.mkdir() + evidence = tmp_path / "configured-evidence" + session = evidence / "synthetic-session" + session.mkdir(parents=True) + monkeypatch.setenv("MISSIONCORE_EVIDENCE_DIR", str(evidence)) + service = XgridsK1CompatibilityService(checkout) + authority_entered = False + + def deny_authority(_reserve: Callable[[], bool]) -> bool: + nonlocal authority_entered + authority_entered = True + return False + + try: + with pytest.raises(ValueError, match="authority"): + service.camera_preview.activate_recording_producer( + "sensor.camera.right", "192.168.1.20", session, + pre_prepare_fence=deny_authority, commit_fence=lambda _commit: False, + ) + assert authority_entered is True + assert service.camera_preview.snapshot()["active_source_id"] is None + finally: + service.close() + + def test_camera_derived_observer_runs_only_after_durable_archive_commit( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/uv.lock b/uv.lock index a1ba119..8b6c463 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,37 @@ version = 1 revision = 3 requires-python = "==3.12.*" +[[package]] +name = "aioice" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "ifaddr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/04/df7286233f468e19e9bedff023b6b246182f0b2ccb04ceeb69b2994021c6/aioice-0.10.2.tar.gz", hash = "sha256:bf236c6829ee33c8e540535d31cd5a066b531cb56de2be94c46be76d68b1a806", size = 44307, upload-time = "2025-11-28T15:56:48.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e3/0d23b1f930c17d371ce1ec36ee529f22fd19ebc2a07fe3418e3d1d884ce2/aioice-0.10.2-py3-none-any.whl", hash = "sha256:14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf", size = 24875, upload-time = "2025-11-28T15:56:47.847Z" }, +] + +[[package]] +name = "aiortc" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aioice" }, + { name = "av" }, + { name = "cryptography" }, + { name = "google-crc32c" }, + { name = "pyee" }, + { name = "pylibsrtp" }, + { name = "pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/9c/4e027bfe0195de0442da301e2389329496745d40ae44d2d7c4571c4290ce/aiortc-1.14.0.tar.gz", hash = "sha256:adc8a67ace10a085721e588e06a00358ed8eaf5f6b62f0a95358ff45628dd762", size = 1180864, upload-time = "2025-10-13T21:40:37.905Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/ab/31646a49209568cde3b97eeade0d28bb78b400e6645c56422c101df68932/aiortc-1.14.0-py3-none-any.whl", hash = "sha256:4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e", size = 93183, upload-time = "2025-10-13T21:40:36.59Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -42,6 +73,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "av" +version = "16.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/cd/3a83ffbc3cc25b39721d174487fb0d51a76582f4a1703f98e46170ce83d4/av-16.1.0.tar.gz", hash = "sha256:a094b4fd87a3721dacf02794d3d2c82b8d712c85b9534437e82a8a978c175ffd", size = 4285203, upload-time = "2026-01-11T07:31:33.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/84/2535f55edcd426cebec02eb37b811b1b0c163f26b8d3f53b059e2ec32665/av-16.1.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:640f57b93f927fba8689f6966c956737ee95388a91bd0b8c8b5e0481f73513d6", size = 26945785, upload-time = "2026-01-09T20:18:34.486Z" }, + { url = "https://files.pythonhosted.org/packages/b6/17/ffb940c9e490bf42e86db4db1ff426ee1559cd355a69609ec1efe4d3a9eb/av-16.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ae3fb658eec00852ebd7412fdc141f17f3ddce8afee2d2e1cf366263ad2a3b35", size = 21481147, upload-time = "2026-01-09T20:18:36.716Z" }, + { url = "https://files.pythonhosted.org/packages/15/c1/e0d58003d2d83c3921887d5c8c9b8f5f7de9b58dc2194356a2656a45cfdc/av-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ee558d9c02a142eebcbe55578a6d817fedfde42ff5676275504e16d07a7f86", size = 39517197, upload-time = "2026-01-11T09:57:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/77/787797b43475d1b90626af76f80bfb0c12cfec5e11eafcfc4151b8c80218/av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", size = 41174337, upload-time = "2026-01-11T09:57:35.792Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/d90df7f1e3b97fc5554cf45076df5045f1e0a6adf13899e10121229b826c/av-16.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8cf065f9d438e1921dc31fc7aa045790b58aee71736897866420d80b5450f62a", size = 40817720, upload-time = "2026-01-11T09:57:39.039Z" }, + { url = "https://files.pythonhosted.org/packages/80/6f/13c3a35f9dbcebafd03fe0c4cbd075d71ac8968ec849a3cfce406c35a9d2/av-16.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a345877a9d3cc0f08e2bc4ec163ee83176864b92587afb9d08dff50f37a9a829", size = 42267396, upload-time = "2026-01-11T09:57:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/275df9607f7fb44317ccb1d4be74827185c0d410f52b6e2cd770fe209118/av-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f49243b1d27c91cd8c66fdba90a674e344eb8eb917264f36117bf2b6879118fd", size = 31752045, upload-time = "2026-01-11T09:57:45.106Z" }, +] + [[package]] name = "bleak" version = "3.0.2" @@ -172,6 +218,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/2b/da036e9f4aeb776833139575fe0774544aa6cd13ba997eea7fbc4ab99852/dbus_fast-5.0.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7d1c42963235cfc015a2d2b8c5fe42b65387493b4ad4ce0ec122601c805e6742", size = 860651, upload-time = "2026-06-05T18:56:10.952Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "fastapi" version = "0.139.0" @@ -208,6 +263,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/e1/8ccb4a985c5baf82947e48cff18483c2125ea11e55e8a28950730ab6c065/foxglove_sdk-0.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:ac881ae307ba432766e6141d9098ced2059838226d225fbeb20de1c57d782a9f", size = 16496466, upload-time = "2026-06-25T00:24:24.906Z" }, ] +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, +] + [[package]] name = "grpcio" version = "1.83.1" @@ -290,6 +358,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -421,6 +498,9 @@ dependencies = [ ] [package.optional-dependencies] +node-device-media = [ + { name = "aiortc" }, +] perception-stream = [ { name = "grpcio" }, ] @@ -435,6 +515,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "aiortc", marker = "extra == 'node-device-media'", specifier = "==1.14.0" }, { name = "bleak", specifier = "==3.0.2" }, { name = "cryptography", specifier = ">=46,<47" }, { name = "fastapi", specifier = ">=0.116,<1" }, @@ -451,7 +532,7 @@ requires-dist = [ { name = "typer", specifier = ">=0.15,<1" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.35,<1" }, ] -provides-extras = ["perception-stream"] +provides-extras = ["perception-stream", "node-device-media"] [package.metadata.requires-dev] dev = [ @@ -618,6 +699,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] +[[package]] +name = "pyee" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/f1/fdedc2c75c3e31a330659c85e5793bb18b3397981fbf0844c6dee5b18926/pyee-14.0.0.tar.gz", hash = "sha256:76dd0f4314ecd27f02dc73589dea7fd3853f9b6176d8ef9b122860657e3602de", size = 98760, upload-time = "2026-08-13T04:26:11.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/12/5347938b1f9a6453f0dbdfcc3e2388a1320ef9b9ec17fbefbc4ab647ea98/pyee-14.0.0-py3-none-any.whl", hash = "sha256:3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d", size = 15553, upload-time = "2026-08-13T04:26:09.916Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -627,6 +720,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pylibsrtp" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/a6/6e532bec974aaecbf9fe4e12538489fb1c28456e65088a50f305aeab9f89/pylibsrtp-1.0.0.tar.gz", hash = "sha256:b39dff075b263a8ded5377f2490c60d2af452c9f06c4d061c7a2b640612b34d4", size = 10858, upload-time = "2025-10-13T16:12:31.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/af/89e61a62fa3567f1b7883feb4d19e19564066c2fcd41c37e08d317b51881/pylibsrtp-1.0.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:822c30ea9e759b333dc1f56ceac778707c51546e97eb874de98d7d378c000122", size = 1865017, upload-time = "2025-10-13T16:12:15.62Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0e/8d215484a9877adcf2459a8b28165fc89668b034565277fd55d666edd247/pylibsrtp-1.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aaad74e5c8cbc1c32056c3767fea494c1e62b3aea2c908eda2a1051389fdad76", size = 2182739, upload-time = "2025-10-13T16:12:17.121Z" }, + { url = "https://files.pythonhosted.org/packages/57/3f/76a841978877ae13eac0d4af412c13bbd5d83b3df2c1f5f2175f2e0f68e5/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9209b86e662ebbd17c8a9e8549ba57eca92a3e87fb5ba8c0e27b8c43cd08a767", size = 2732922, upload-time = "2025-10-13T16:12:18.348Z" }, + { url = "https://files.pythonhosted.org/packages/0e/14/cf5d2a98a66fdfe258f6b036cda570f704a644fa861d7883a34bc359501e/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc", size = 2434534, upload-time = "2025-10-13T16:12:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/bd/08/a3f6e86c04562f7dce6717cd2206a0f84ca85c5e38121d998e0e330194c3/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_28_i686.whl", hash = "sha256:81fb8879c2e522021a7cbd3f4bda1b37c192e1af939dfda3ff95b4723b329663", size = 2345818, upload-time = "2025-10-13T16:12:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d5/130c2b5b4b51df5631684069c6f0a6761c59d096a33d21503ac207cf0e47/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4ddb562e443cf2e557ea2dfaeef0d7e6b90e96dd38eb079b4ab2c8e34a79f50b", size = 2774490, upload-time = "2025-10-13T16:12:22.659Z" }, + { url = "https://files.pythonhosted.org/packages/91/e3/715a453bfee3bea92a243888ad359094a7727cc6d393f21281320fe7798c/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f02e616c9dfab2b03b32d8cc7b748f9d91814c0211086f987629a60f05f6e2cc", size = 2372603, upload-time = "2025-10-13T16:12:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/e3/56/52fa74294254e1f53a4ff170ee2006e57886cf4bb3db46a02b4f09e1d99f/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c134fa09e7b80a5b7fed626230c5bc257fd771bd6978e754343e7a61d96bc7e6", size = 2451269, upload-time = "2025-10-13T16:12:25.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/51/2e9b34f484cbdd3bac999bf1f48b696d7389433e900639089e8fc4e0da0d/pylibsrtp-1.0.0-cp310-abi3-win32.whl", hash = "sha256:bae377c3b402b17b9bbfbfe2534c2edba17aa13bea4c64ce440caacbe0858b55", size = 1247503, upload-time = "2025-10-13T16:12:27.39Z" }, + { url = "https://files.pythonhosted.org/packages/c3/70/43db21af194580aba2d9a6d4c7bd8c1a6e887fa52cd810b88f89096ecad2/pylibsrtp-1.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:8d6527c4a78a39a8d397f8862a8b7cdad4701ee866faf9de4ab8c70be61fd34d", size = 1601659, upload-time = "2025-10-13T16:12:29.037Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, +] + [[package]] name = "pyobjc-core" version = "12.2.1" @@ -674,6 +789,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/8f/42cfa987c07a2b5ce8c236a42b0fb388b8807dac72c25e004cd4905ea9a3/pyobjc_framework_libdispatch-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8f41b5021ff70bc51220a79b41ebd1eacb55fe3ceb67448594f30e491a2c42a5", size = 15656, upload-time = "2026-06-19T16:12:50.361Z" }, ] +[[package]] +name = "pyopenssl" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, +] + [[package]] name = "pytest" version = "8.4.2"