diff --git a/apps/control-station/public/vendor/playcanvas/draco/1.5.7/BUILD_INFO.md b/apps/control-station/public/vendor/playcanvas/draco/1.5.7/BUILD_INFO.md new file mode 100644 index 0000000..e837ac8 --- /dev/null +++ b/apps/control-station/public/vendor/playcanvas/draco/1.5.7/BUILD_INFO.md @@ -0,0 +1,8 @@ +# Draco glTF decoder 1.5.7 + +Vendored from Google's immutable versioned Draco decoder distribution for the +PlayCanvas `DracoDecoderModule`. Runtime loading remains local to Mission Core. + +- `draco_wasm_wrapper_gltf.js`: `8bb2952d2ba7d67e1414f8df819410cb0434a666be53f671fff75f68843d76f6` +- `draco_decoder_gltf.wasm`: `712db3449ae2041d6e8a224c395bda6cedb49e51322fae38b7db9beb8b381889` +- Upstream base: `https://www.gstatic.com/draco/versioned/decoders/1.5.7/` diff --git a/apps/control-station/public/vendor/playcanvas/draco/1.5.7/draco_decoder_gltf.wasm b/apps/control-station/public/vendor/playcanvas/draco/1.5.7/draco_decoder_gltf.wasm new file mode 100644 index 0000000..26aa5d3 Binary files /dev/null and b/apps/control-station/public/vendor/playcanvas/draco/1.5.7/draco_decoder_gltf.wasm differ diff --git a/apps/control-station/public/vendor/playcanvas/draco/1.5.7/draco_wasm_wrapper_gltf.js b/apps/control-station/public/vendor/playcanvas/draco/1.5.7/draco_wasm_wrapper_gltf.js new file mode 100644 index 0000000..43f1556 --- /dev/null +++ b/apps/control-station/public/vendor/playcanvas/draco/1.5.7/draco_wasm_wrapper_gltf.js @@ -0,0 +1,116 @@ +var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE? +$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))}; +$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)}; +var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q=y}},"es6","es3"); +$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(kp;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3"); +$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5"); +$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5"); +var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e); +e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)}); +if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+ +12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+ +16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0=d?b++:2047>=d?b+=2:55296<=d&&57343>= +d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"=== +typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]= +this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr= +Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3=e[1]?!0:0!=e[0]||10< +e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e, +b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1>>=0;if(2147483648=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)} +function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")|| +P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0= +function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0= +a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0= +a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0= +function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0= +a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0= +function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1= +function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb= +a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0= +a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb= +a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null, +arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0= +a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0= +a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0= +a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib= +a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0= +function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1= +a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1= +function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0= +a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0= +function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0= +a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0= +function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2= +a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null, +arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0= +function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2= +a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)}, +oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3= +function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc= +a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3= +a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3= +a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3= +a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1= +a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM= +function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM= +a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION= +a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD= +a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD= +a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc= +a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32= +a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32= +function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT= +a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER= +function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)}; +a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0=r.size?(0>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;gb.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule); diff --git a/apps/control-station/src/components/simulation/PlayCanvasRuntime.ts b/apps/control-station/src/components/simulation/PlayCanvasRuntime.ts index 3201b56..d9629d8 100644 --- a/apps/control-station/src/components/simulation/PlayCanvasRuntime.ts +++ b/apps/control-station/src/components/simulation/PlayCanvasRuntime.ts @@ -11,16 +11,21 @@ import { StandardMaterial, Vec2, Vec3, + WasmModule, type ContainerResource, type RenderComponent, type ScriptType, } from "playcanvas"; import { CameraControls } from "playcanvas/scripts/esm/camera-controls.mjs"; -import type { SimulationWorldManifest } from "../../core/simulation/projects"; +import type { + SimulationTransformAxis, + SimulationViewerQuality, + SimulationWorldManifest, +} from "../../core/simulation/projects"; export type SimulationViewMode = "visual" | "collision" | "combined"; -export type SimulationQuality = "low" | "medium" | "high" | "ultra" | "maximum"; +export type SimulationQuality = SimulationViewerQuality; export type SimulationLayer = "visual" | "collision"; export type SimulationCameraAxis = "horizontal" | "vertical"; @@ -38,6 +43,27 @@ export const PLAYCANVAS_X_180_TRANSFORM = [ 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 interface SimulationRuntime { mount(canvas: HTMLCanvasElement): Promise; loadWorld(manifest: SimulationWorldManifest): Promise; @@ -60,6 +86,7 @@ type CameraController = ScriptType & Pick & { const HOME_POSITION = new Vec3(0, 1, 0); const HOME_FOCUS = new Vec3(1, 1, 0); +const DRACO_DECODER_ROOT = "/vendor/playcanvas/draco/1.5.7"; const QUALITY_PROFILES: Record { this.viewMode = mode; this.applyViewMode(); - if (mode !== "visual" && !this.collisionEntity) { - await this.ensureCollision(); - this.applyViewMode(); + try { + if (mode !== "visual" && !this.collisionEntity) { + await this.ensureCollision(); + this.applyViewMode(); + } + } finally { + this.canvas?.focus({ preventScroll: true }); } } @@ -444,6 +476,15 @@ function preventContextMenu(event: Event): void { event.preventDefault(); } +function configureDracoDecoder(): void { + if (WasmModule.getConfig("DracoDecoderModule")) return; + WasmModule.setConfig("DracoDecoderModule", { + glueUrl: `${DRACO_DECODER_ROOT}/draco_wasm_wrapper_gltf.js`, + wasmUrl: `${DRACO_DECODER_ROOT}/draco_decoder_gltf.wasm`, + numWorkers: 2, + }); +} + function focusCanvas(event: Event): void { (event.currentTarget as HTMLCanvasElement | null)?.focus({ preventScroll: true }); } diff --git a/apps/control-station/src/components/simulation/SimulationViewport.tsx b/apps/control-station/src/components/simulation/SimulationViewport.tsx index b6629ce..32581de 100644 --- a/apps/control-station/src/components/simulation/SimulationViewport.tsx +++ b/apps/control-station/src/components/simulation/SimulationViewport.tsx @@ -10,40 +10,101 @@ import { Switch, } from "@nodedc/ui-react"; -import type { SimulationProject } from "../../core/simulation/projects"; import { - PLAYCANVAS_IDENTITY_TRANSFORM, - PLAYCANVAS_X_180_TRANSFORM, + saveSimulationViewerSettings, + type SimulationProject, + type SimulationTransformAxis, + type SimulationViewerSettings, +} from "../../core/simulation/projects"; +import { + createLatestAsyncCommitter, + type LatestAsyncCommitter, +} from "../../core/runtime/latestAsyncCommitter"; +import { PlayCanvasRuntime, + playCanvasAxisTransform, type SimulationQuality, type SimulationViewMode, } from "./PlayCanvasRuntime"; -export function SimulationViewport({ project }: { project: SimulationProject }) { +const AXIS_OPTIONS: Array<{ value: SimulationTransformAxis; label: string }> = [ + { value: "x", label: "X" }, + { value: "y", label: "Y" }, + { value: "z", label: "Z" }, +]; + +export function SimulationViewport({ + project, + onProjectChange, +}: { + project: SimulationProject; + onProjectChange?: (project: SimulationProject) => void; +}) { const canvasRef = useRef(null); const runtimeRef = useRef(null); - const cameraInversionRef = useRef({ horizontal: true, vertical: false }); + const viewerSettingsRef = useRef(project.viewerSettings); + const settingsCommitterRef = useRef | null>(null); const settingsId = useId(); const [state, setState] = useState<"mounting" | "loading" | "ready" | "failed">("mounting"); const [error, setError] = useState(null); const [viewMode, setViewMode] = useState("visual"); - const [quality, setQuality] = useState("high"); + 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( - () => isX180Transform(project.worldManifest?.transforms.worldFromVisual), + 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 [horizontalInverted, setHorizontalInverted] = useState( + project.viewerSettings.camera.invertHorizontal, ); - const [collisionInverted, setCollisionInverted] = useState( - () => isX180Transform(project.worldManifest?.transforms.worldFromCollision), + const [verticalInverted, setVerticalInverted] = useState( + project.viewerSettings.camera.invertVertical, ); - const [horizontalInverted, setHorizontalInverted] = useState(true); - const [verticalInverted, setVerticalInverted] = useState(false); + const [settingsError, setSettingsError] = useState(null); useEffect(() => { - setVisualInverted(isX180Transform(project.worldManifest?.transforms.worldFromVisual)); - setCollisionInverted(isX180Transform(project.worldManifest?.transforms.worldFromCollision)); + 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); + setHorizontalInverted(settings.camera.invertHorizontal); + setVerticalInverted(settings.camera.invertVertical); + setSettingsError(null); setSettingsOpen(false); - }, [project.projectId, project.worldManifest]); + }, [project.projectId]); + + useEffect(() => { + let active = true; + const committer = createLatestAsyncCommitter({ + async commit(settings) { + try { + const saved = await saveSimulationViewerSettings(project.projectId, settings); + if (active) onProjectChange?.(saved); + return true; + } catch (caught) { + if (active) { + setSettingsError( + caught instanceof Error ? caught.message : "Не удалось сохранить настройки сцены.", + ); + } + return false; + } + }, + onSettled({ applied, superseded }) { + if (active && applied && !superseded) setSettingsError(null); + }, + }); + settingsCommitterRef.current = committer; + return () => { + active = false; + committer.dispose(); + if (settingsCommitterRef.current === committer) settingsCommitterRef.current = null; + }; + }, [onProjectChange, project.projectId]); useEffect(() => { const canvas = canvasRef.current; @@ -56,10 +117,20 @@ export function SimulationViewport({ project }: { project: SimulationProject }) setError(null); void runtime.mount(canvas).then(async () => { if (cancelled) return; - runtime.setCameraInversion("horizontal", cameraInversionRef.current.horizontal); - runtime.setCameraInversion("vertical", cameraInversionRef.current.vertical); + const settings = viewerSettingsRef.current; + runtime.setQuality(settings.quality); + runtime.setCameraInversion("horizontal", settings.camera.invertHorizontal); + runtime.setCameraInversion("vertical", settings.camera.invertVertical); setState("loading"); await runtime.loadWorld(manifest); + runtime.setLayerWorldTransform( + "visual", + playCanvasAxisTransform(settings.visual.axis, settings.visual.inverted), + ); + runtime.setLayerWorldTransform( + "collision", + playCanvasAxisTransform(settings.collision.axis, settings.collision.inverted), + ); if (!cancelled) setState("ready"); }).catch((caught: unknown) => { if (cancelled) return; @@ -74,6 +145,10 @@ export function SimulationViewport({ project }: { project: SimulationProject }) }, [project.projectId, project.worldManifest]); const collisionAvailable = project.worldManifest?.collision.available ?? false; + const commitViewerSettings = (settings: SimulationViewerSettings) => { + viewerSettingsRef.current = settings; + settingsCommitterRef.current?.enqueue(settings); + }; return (
@@ -99,6 +174,7 @@ export function SimulationViewport({ project }: { project: SimulationProject }) onChange={(next) => { setQuality(next); runtimeRef.current?.setQuality(next); + commitViewerSettings({ ...viewerSettingsRef.current, quality: next }); }} options={[ { value: "maximum", label: "Максимум", description: "До 4 млн сплатов · Retina до 2× · 60 FPS" }, @@ -171,30 +247,82 @@ export function SimulationViewport({ project }: { project: SimulationProject }) Коррекция слоёв не меняет координаты камеры, навигации и будущей физики.

- { - setVisualInverted(checked); - runtimeRef.current?.setLayerWorldTransform( - "visual", - checked ? PLAYCANVAS_X_180_TRANSFORM : PLAYCANVAS_IDENTITY_TRANSFORM, - ); - }} - /> - { - setCollisionInverted(checked); - runtimeRef.current?.setLayerWorldTransform( - "collision", - checked ? PLAYCANVAS_X_180_TRANSFORM : PLAYCANVAS_IDENTITY_TRANSFORM, - ); - }} - /> +
+ { + 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 }, + }); + }} + /> +
@@ -208,9 +336,12 @@ export function SimulationViewport({ project }: { project: SimulationProject }) disabled={state !== "ready"} label="Инверсия управления по горизонтали" onChange={(checked) => { - cameraInversionRef.current.horizontal = checked; setHorizontalInverted(checked); runtimeRef.current?.setCameraInversion("horizontal", checked); + commitViewerSettings({ + ...viewerSettingsRef.current, + camera: { ...viewerSettingsRef.current.camera, invertHorizontal: checked }, + }); }} /> { - cameraInversionRef.current.vertical = checked; setVerticalInverted(checked); runtimeRef.current?.setCameraInversion("vertical", checked); + commitViewerSettings({ + ...viewerSettingsRef.current, + camera: { ...viewerSettingsRef.current.camera, invertVertical: checked }, + }); }} />
+ {settingsError ? ( +

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

+ ) : null} ) : null} @@ -255,8 +394,3 @@ export function SimulationViewport({ project }: { project: SimulationProject }) ); } - -function isX180Transform(transform: number[] | undefined): boolean { - if (!transform || transform.length !== 16) return false; - return transform.every((value, index) => value === PLAYCANVAS_X_180_TRANSFORM[index]); -} diff --git a/apps/control-station/src/core/simulation/projects.ts b/apps/control-station/src/core/simulation/projects.ts index daa2739..99982d3 100644 --- a/apps/control-station/src/core/simulation/projects.ts +++ b/apps/control-station/src/core/simulation/projects.ts @@ -1,4 +1,6 @@ export type SimulationSceneType = "interior" | "outdoor" | "object"; +export type SimulationTransformAxis = "x" | "y" | "z"; +export type SimulationViewerQuality = "low" | "medium" | "high" | "ultra" | "maximum"; export type SimulationProjectStatus = | "uploading" | "queued" @@ -32,6 +34,14 @@ export interface SimulationWorldManifest { }; } +export interface SimulationViewerSettings { + schemaVersion: "missioncore.simulation-viewer-settings/v1"; + quality: SimulationViewerQuality; + visual: { inverted: boolean; axis: SimulationTransformAxis }; + collision: { inverted: boolean; axis: SimulationTransformAxis }; + camera: { invertHorizontal: boolean; invertVertical: boolean }; +} + export interface SimulationProject { schemaVersion: "missioncore.simulation-project/v1"; projectId: string; @@ -60,6 +70,7 @@ export interface SimulationProject { byteLength: number; }>; worldManifest: SimulationWorldManifest | null; + viewerSettings: SimulationViewerSettings; error: string | null; createdAtUtc: string; updatedAtUtc: string; @@ -223,6 +234,30 @@ export async function updateSimulationProject( return parseProject(await jsonResponse(response)); } +export async function saveSimulationViewerSettings( + projectId: string, + settings: SimulationViewerSettings, +): Promise { + const response = await fetch( + `${API_ROOT}/${encodeURIComponent(projectId)}/viewer-settings`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + schema_version: settings.schemaVersion, + quality: settings.quality, + visual: settings.visual, + collision: settings.collision, + camera: { + invert_horizontal: settings.camera.invertHorizontal, + invert_vertical: settings.camera.invertVertical, + }, + }), + }, + ); + return parseProject(await jsonResponse(response)); +} + export async function retrySimulationProject(projectId: string): Promise { const response = await fetch(`${API_ROOT}/${encodeURIComponent(projectId)}/build`, { method: "POST", @@ -251,7 +286,7 @@ function parseProject(value: unknown): SimulationProject { const record = objectValue(value, "simulation project"); exactKeys(record, [ "schema_version", "project_id", "name", "scene_type", "status", "source", "provider", - "artifacts", "world_manifest", "error", "created_at_utc", "updated_at_utc", + "artifacts", "world_manifest", "viewer_settings", "error", "created_at_utc", "updated_at_utc", ], "simulation project"); if (record.schema_version !== "missioncore.simulation-project/v1") { throw new Error("Проект симуляции вернул неподдерживаемую версию."); @@ -301,12 +336,47 @@ function parseProject(value: unknown): SimulationProject { }; }), worldManifest: record.world_manifest === null ? null : parseWorldManifest(record.world_manifest), + viewerSettings: parseViewerSettings(record.viewer_settings), error: nullableString(record.error, "project error"), createdAtUtc: stringValue(record.created_at_utc, "created at"), updatedAtUtc: stringValue(record.updated_at_utc, "updated at"), }; } +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") { + 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"); + return { + schemaVersion: "missioncore.simulation-viewer-settings/v1", + quality: qualityValue(record.quality), + visual, + collision, + camera: { + invertHorizontal: booleanValue(camera.invert_horizontal, "horizontal camera inversion"), + invertVertical: booleanValue(camera.invert_vertical, "vertical camera inversion"), + }, + }; +} + +function parseLayerViewerSettings( + value: unknown, + label: string, +): { inverted: boolean; axis: SimulationTransformAxis } { + const record = objectValue(value, label); + exactKeys(record, ["inverted", "axis"], label); + return { + inverted: booleanValue(record.inverted, `${label} inversion`), + axis: axisValue(record.axis), + }; +} + function parseWorldManifest(value: unknown): SimulationWorldManifest { const record = objectValue(value, "world manifest"); const visual = objectValue(record.visual, "visual manifest"); @@ -396,6 +466,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 qualityValue(value: unknown): SimulationViewerQuality { + if (["low", "medium", "high", "ultra", "maximum"].includes(String(value))) { + return value as SimulationViewerQuality; + } + return invalid("viewer quality"); +} + function statusValue(value: unknown): SimulationProjectStatus { if (["uploading", "queued", "processing", "importing", "ready", "failed"].includes(String(value))) { return value as SimulationProjectStatus; diff --git a/apps/control-station/src/styles/simulation.css b/apps/control-station/src/styles/simulation.css index 22716d1..e32b1ab 100644 --- a/apps/control-station/src/styles/simulation.css +++ b/apps/control-station/src/styles/simulation.css @@ -440,6 +440,25 @@ gap: 0.55rem; } +.simulation-viewport__settings-layer-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 4.5rem; + align-items: center; + gap: 0.55rem; +} + +.simulation-viewport__axis-select { + width: 4.5rem; + min-width: 0; +} + +.simulation-viewport__settings-error { + border-radius: 0.65rem; + background: rgb(var(--nodedc-danger-rgb) / 0.08); + color: var(--nodedc-danger) !important; + padding: 0.55rem 0.65rem; +} + .simulation-viewport__toolbar-balance { min-width: 0; } diff --git a/apps/control-station/src/workspaces/simulation/SimulationWorkspace.tsx b/apps/control-station/src/workspaces/simulation/SimulationWorkspace.tsx index 4b6f164..c5eccd1 100644 --- a/apps/control-station/src/workspaces/simulation/SimulationWorkspace.tsx +++ b/apps/control-station/src/workspaces/simulation/SimulationWorkspace.tsx @@ -78,6 +78,12 @@ export function SimulationWorkspace() { setEditing(null); }; + const acceptViewerProject = useCallback((project: SimulationProject) => { + setProjects((current) => current.map((item) => ( + item.projectId === project.projectId ? project : item + ))); + }, []); + const confirmDelete = async () => { if (!deleting) return; await deleteSimulationProject(deleting.projectId); @@ -122,7 +128,10 @@ export function SimulationWorkspace() { {selected.status === "ready" && selected.worldManifest ? ( - + ) : ( {selected.status === "failed" ? : } diff --git a/apps/control-station/test/simulationWorkspace.test.mjs b/apps/control-station/test/simulationWorkspace.test.mjs index ceb1ef0..1fb5e08 100644 --- a/apps/control-station/test/simulationWorkspace.test.mjs +++ b/apps/control-station/test/simulationWorkspace.test.mjs @@ -81,6 +81,10 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity 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, /DracoDecoderModule/); + assert.match(runtime, /draco_decoder_gltf\.wasm/); assert.match(runtime, /ensureCollision/); assert.match(runtime, /app\.root\.addChild/); assert.match(runtime, /dispose\(\)/); @@ -88,11 +92,15 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity assert.match(viewport, /Collision недоступен/); assert.match(viewport, /Для этой сборки collision GLB не был запрошен/); assert.match(viewport, /\("high"\)/); + assert.match(viewport, /useState\(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, /Ось инверсии collision-слоя/); + assert.match(viewport, /saveSimulationViewerSettings/); + assert.match(viewport, /onProjectChange\?\.\(saved\)/); }); diff --git a/src/k1link/service_watchdog.py b/src/k1link/service_watchdog.py index a69f7a1..a2e9cfa 100644 --- a/src/k1link/service_watchdog.py +++ b/src/k1link/service_watchdog.py @@ -28,10 +28,18 @@ class MissionCoreWatchdogError(RuntimeError): @dataclass(frozen=True, slots=True) class MissionCoreWatchdogPolicy: - startup_grace_seconds: float = 45.0 + # Cold startup on the 18 GB control laptop includes validating the durable + # recorded-evidence catalog and can exceed two minutes under storage load. + # This applies only before the first probe; steady-state failures retain the + # much tighter bounded policy below. + startup_grace_seconds: float = 300.0 probe_interval_seconds: float = 2.0 - probe_timeout_seconds: float = 1.0 - consecutive_failure_limit: int = 3 + # Source hashing and resumable multi-gigabyte uploads can briefly delay the + # synchronous readiness projection without making the service unhealthy. + # Six bounded five-second probes still fail closed, but do not turn normal + # storage pressure into a restart loop that discards upload progress. + probe_timeout_seconds: float = 5.0 + consecutive_failure_limit: int = 6 graceful_shutdown_seconds: float = 12.0 def __post_init__(self) -> None: diff --git a/src/k1link/simulation/projects.py b/src/k1link/simulation/projects.py index 2ae2ce4..51dfb43 100644 --- a/src/k1link/simulation/projects.py +++ b/src/k1link/simulation/projects.py @@ -26,6 +26,7 @@ from k1link.simulation.gaussian_pipeline_gateway import ( PROJECT_SCHEMA: Final = "missioncore.simulation-project/v1" WORLD_MANIFEST_SCHEMA: Final = "missioncore.simulation-world-manifest/v1" +VIEWER_SETTINGS_SCHEMA: Final = "missioncore.simulation-viewer-settings/v1" PROJECT_ID_PATTERN: Final = re.compile(r"^sim-[a-f0-9]{32}$") SOURCE_FILE_ID_PATTERN: Final = re.compile(r"^src-[0-9]{5}-[a-f0-9]{8}$") PROVIDER_JOB_ID_PATTERN: Final = re.compile(r"^gsp-[0-9]{14}-[a-f0-9]{8}$") @@ -142,6 +143,7 @@ class SimulationProjectStore: }, "artifacts": [], "world_manifest": None, + "viewer_settings": _default_viewer_settings(), "error": None, "created_at_utc": now, "updated_at_utc": now, @@ -189,6 +191,19 @@ class SimulationProjectStore: self._write(document) return document + def update_viewer_settings( + self, + project_id: str, + settings: dict[str, Any], + ) -> dict[str, Any]: + normalized = _viewer_settings(settings) + with self._lock: + document = self._read(project_id) + document["viewer_settings"] = normalized + document["updated_at_utc"] = utc_now_iso() + self._write(document) + return document + def upload_state(self, project_id: str, file_id: str) -> tuple[int, int]: with self._lock: document = self._read(project_id) @@ -239,7 +254,7 @@ class SimulationProjectStore: def begin_build(self, project_id: str) -> dict[str, Any]: with self._lock: document = self._read(project_id) - if document["status"] not in {"uploading", "failed"}: + if document["status"] not in {"uploading", "failed", "ready"}: raise SimulationProjectConflictError("simulation project cannot start a build") if any( int(item["uploaded_bytes"]) != int(item["byte_length"]) @@ -371,6 +386,7 @@ class SimulationProjectStore: or document.get("project_id") != project_id ): raise SimulationProjectError("persisted simulation project identity is invalid") + document.setdefault("viewer_settings", _default_viewer_settings()) return document def _write(self, document: dict[str, Any]) -> None: @@ -421,7 +437,7 @@ class SimulationProjectService: def begin_build(self, project_id: str) -> dict[str, Any]: project = self.store.get(project_id) - if project.get("status") == "failed": + if project.get("status") in {"failed", "ready"}: job_id = project["provider"].get("job_id") if isinstance(job_id, str): provider = self.provider_factory() @@ -616,7 +632,7 @@ 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], }, } @@ -628,11 +644,66 @@ def _collision_profile(scene_type: str) -> dict[str, Any]: "seed_position": [0, 1, 0], "capsule_height": 1.6, "capsule_radius": 0.2, - "voxel_size": 0.05, + "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"}, + "camera": { + "invert_horizontal": True, + "invert_vertical": False, + }, + } + + +def _viewer_settings(value: object) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != { + "schema_version", "quality", "visual", "collision", "camera" + }: + raise SimulationProjectError("simulation viewer settings contract is invalid") + if value.get("schema_version") != VIEWER_SETTINGS_SCHEMA: + raise SimulationProjectError("simulation viewer settings version is unsupported") + quality = value.get("quality") + if quality not in {"low", "medium", "high", "ultra", "maximum"}: + raise SimulationProjectError("simulation viewer quality is invalid") + + def layer(name: str) -> dict[str, Any]: + raw = value.get(name) + if not isinstance(raw, dict) or set(raw) != {"inverted", "axis"}: + 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"}: + raise SimulationProjectError(f"simulation {name} transform is invalid") + return {"inverted": inverted, "axis": axis} + + camera = value.get("camera") + 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") + return { + "schema_version": VIEWER_SETTINGS_SCHEMA, + "quality": quality, + "visual": layer("visual"), + "collision": layer("collision"), + "camera": { + "invert_horizontal": horizontal, + "invert_vertical": vertical, + }, + } + + def _project_name(value: str) -> str: normalized = " ".join(value.split()) if not 1 <= len(normalized) <= 120: diff --git a/src/k1link/web/simulation_projects_api.py b/src/k1link/web/simulation_projects_api.py index ec76746..97a8581 100644 --- a/src/k1link/web/simulation_projects_api.py +++ b/src/k1link/web/simulation_projects_api.py @@ -12,6 +12,7 @@ from k1link.simulation.gaussian_pipeline_gateway import GaussianPipelineGatewayE from k1link.simulation.projects import ( MAX_UPLOAD_CHUNK_BYTES, PROJECT_SCHEMA, + VIEWER_SETTINGS_SCHEMA, SimulationProjectConflictError, SimulationProjectError, SimulationProjectNotFoundError, @@ -45,6 +46,30 @@ class SimulationProjectUpdate(BaseModel): scene_type: Literal["interior", "outdoor", "object"] +class SimulationLayerViewerSettings(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + inverted: bool + axis: Literal["x", "y", "z"] + + +class SimulationCameraViewerSettings(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + invert_horizontal: bool + invert_vertical: bool + + +class SimulationViewerSettings(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + schema_version: Literal["missioncore.simulation-viewer-settings/v1"] = VIEWER_SETTINGS_SCHEMA + quality: Literal["low", "medium", "high", "ultra", "maximum"] + visual: SimulationLayerViewerSettings + collision: SimulationLayerViewerSettings + camera: SimulationCameraViewerSettings + + class SimulationProjectDocument(BaseModel): model_config = ConfigDict(extra="forbid") @@ -57,6 +82,7 @@ class SimulationProjectDocument(BaseModel): provider: dict[str, Any] artifacts: list[dict[str, Any]] world_manifest: dict[str, Any] | None + viewer_settings: SimulationViewerSettings error: str | None created_at_utc: str updated_at_utc: str @@ -117,6 +143,21 @@ def build_simulation_projects_router( except SimulationProjectError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc + @router.put( + "/projects/{project_id}/viewer-settings", + response_model=SimulationProjectDocument, + ) + def update_viewer_settings( + project_id: str, + request: SimulationViewerSettings, + ) -> dict[str, Any]: + try: + return store.update_viewer_settings(project_id, request.model_dump()) + except SimulationProjectNotFoundError as exc: + raise HTTPException(status_code=404, detail="Проект симуляции не найден.") from exc + except SimulationProjectError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + @router.head("/projects/{project_id}/source/{file_id}") def source_upload_state(project_id: str, file_id: str) -> Response: try: diff --git a/tests/test_simulation_projects.py b/tests/test_simulation_projects.py index c598f2a..9375f97 100644 --- a/tests/test_simulation_projects.py +++ b/tests/test_simulation_projects.py @@ -201,7 +201,7 @@ def test_service_materializes_world_manifest_and_deletes_both_copies(tmp_path: P 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 @@ -216,7 +216,7 @@ def test_service_materializes_world_manifest_and_deletes_both_copies(tmp_path: P "seed_position": [0, 1, 0], "capsule_height": 1.6, "capsule_radius": 0.2, - "voxel_size": 0.05, + "voxel_size": 0.1, "mesh_shape": "smooth", } service.delete(project["project_id"]) @@ -281,6 +281,41 @@ def test_failed_project_retries_from_retained_source_and_releases_old_job(tmp_pa assert queued["source"]["uploaded_byte_length"] == queued["source"]["total_byte_length"] +def test_ready_project_rebuilds_from_retained_source_and_preserves_viewer_settings( + tmp_path: Path, +) -> None: + store = SimulationProjectStore(tmp_path) + project = store.create( + name="Rebuild scene", + scene_type="outdoor", + source_kind="folder", + files=_folder_files(), + ) + _upload_all(store, project) + store.begin_build(project["project_id"]) + provider = _ReadyProvider() + service = SimulationProjectService( + store, + provider_factory=lambda: provider, + ) # type: ignore[arg-type] + service.process(project["project_id"]) + settings = { + "schema_version": "missioncore.simulation-viewer-settings/v1", + "quality": "medium", + "visual": {"inverted": True, "axis": "z"}, + "collision": {"inverted": True, "axis": "y"}, + "camera": {"invert_horizontal": False, "invert_vertical": True}, + } + store.update_viewer_settings(project["project_id"], settings) + + queued = service.begin_build(project["project_id"]) + + assert provider.deleted == ["gsp-20260826000000-deadbeef"] + assert queued["status"] == "queued" + assert queued["viewer_settings"] == settings + assert queued["source"]["uploaded_byte_length"] == queued["source"]["total_byte_length"] + + def test_api_exposes_same_origin_resumable_upload_contract(tmp_path: Path) -> None: store = SimulationProjectStore(tmp_path) service = SimulationProjectService(store, provider_factory=lambda: None) @@ -296,6 +331,13 @@ def test_api_exposes_same_origin_resumable_upload_contract(tmp_path: Path) -> No }) assert created.status_code == 201 project = created.json() + assert project["viewer_settings"] == { + "schema_version": "missioncore.simulation-viewer-settings/v1", + "quality": "high", + "visual": {"inverted": True, "axis": "x"}, + "collision": {"inverted": True, "axis": "y"}, + "camera": {"invert_horizontal": True, "invert_vertical": False}, + } source_file = project["source"]["files"][0] upload_url = ( f"/api/v1/simulation-worlds/projects/{project['project_id']}" @@ -311,3 +353,21 @@ def test_api_exposes_same_origin_resumable_upload_contract(tmp_path: Path) -> No assert second.status_code == 204 catalog = client.get("/api/v1/simulation-worlds/projects").json() assert catalog["projects"][0]["project_id"] == project["project_id"] + + settings = { + "schema_version": "missioncore.simulation-viewer-settings/v1", + "quality": "medium", + "visual": {"inverted": True, "axis": "z"}, + "collision": {"inverted": False, "axis": "x"}, + "camera": {"invert_horizontal": False, "invert_vertical": True}, + } + saved = client.put( + f"/api/v1/simulation-worlds/projects/{project['project_id']}/viewer-settings", + json=settings, + ) + assert saved.status_code == 200 + assert saved.json()["viewer_settings"] == settings + persisted = client.get( + f"/api/v1/simulation-worlds/projects/{project['project_id']}" + ).json() + assert persisted["viewer_settings"] == settings