{const o=await n;return r&&(pr.set(e,o),st.diagnosticTracing&&De(`imported ES6 module '${e}' from '${t}'`)),o}))}function dr(){st.assert_runtime_running(),ot.mono_wasm_bindings_is_ready||ut(!1,"The runtime must be initialized.")}function fr(e){e()}const _r="function"==typeof globalThis.WeakRef;function mr(e){return _r?new WeakRef(e):function(e){return{deref:()=>e,dispose:()=>{e=null}}}(e)}function hr(e,t,n,r,o,s,a){const i=`[${t}] ${n}.${r}:${o}`,c=Bt();st.diagnosticTracing&&De(`Binding [JSExport] ${n}.${r}:${o} from ${t} assembly`);const l=On(a);2!==l&&ut(!1,`Signature version ${l} mismatch.`);const p=Cn(a),u=new Array(p);for(let e=0;e0}function $r(e){return e<-1}wr&&(kr=new globalThis.FinalizationRegistry(Pr));const Lr=Symbol.for("wasm js_owned_gc_handle"),Rr=Symbol.for("wasm cs_owned_js_handle"),Br=Symbol.for("wasm do_not_force_dispose");function Nr(e){return jr(e)?Sr[e]:Ar(e)?vr[0-e]:null}function Cr(e){if(dr(),e[Rr])return e[Rr];const t=Ur.length?Ur.pop():Er++;return Sr[t]=e,Object.isExtensible(e)&&(e[Rr]=t),t}function Or(e){let t;jr(e)?(t=Sr[e],Sr[e]=void 0,Ur.push(e)):Ar(e)&&(t=vr[0-e],vr[0-e]=void 0),null==t&&ut(!1,"ObjectDisposedException"),void 0!==t[Rr]&&(t[Rr]=void 0)}function Dr(e,t){dr(),e[Lr]=t,wr&&kr.register(e,t,e);const n=mr(e);Tr.set(t,n)}function Fr(e,t,r){var o;dr(),e&&(t=e[Lr],e[Lr]=p,wr&&kr.unregister(e)),t!==p&&Tr.delete(t)&&!r&&st.is_runtime_running()&&!zr&&function(e){e||ut(!1,"Must be valid gc_handle"),st.assert_runtime_running();const t=Xe.stackSave();try{const t=xn(3),r=In(t,2);Mn(r,14),Xn(r,e),n&&!$r(e)&&_n.isUI||gn(mn.ReleaseJSOwnedObjectByGCHandle,t)}finally{Xe.stackRestore(t)}}(t),$r(t)&&(o=t,xr.push(o))}function Mr(e){const t=e[Lr];if(t==p)throw new Error("Assert failed: ObjectDisposedException");return t}function Pr(e){st.is_runtime_running()&&Fr(null,e)}function Vr(e){if(!e)return null;const t=Tr.get(e);return t?t.deref():null}let zr=!1;function Hr(e,t){let n=!1,r=!1;zr=!0;let o=0,s=0,a=0,i=0;const c=[...Tr.keys()];for(const e of c){const r=Tr.get(e),o=r&&r.deref();if(wr&&o&&kr.unregister(o),o){const s="boolean"==typeof o[Br]&&o[Br];if(t&&Me(`Proxy of C# ${typeof o} with GCHandle ${e} was still alive. ${s?"keeping":"disposing"}.`),s)n=!0;else{const t=st.getPromiseController(o);t&&t.reject(new Error("WebWorker which is origin of the Task is being terminated.")),"function"==typeof o.dispose&&o.dispose(),o[Lr]===e&&(o[Lr]=p),!_r&&r&&r.dispose(),a++}}}n||(Tr.clear(),wr&&(kr=new globalThis.FinalizationRegistry(Pr)));const l=(e,n)=>{const o=n[e],s=o&&"boolean"==typeof o[Br]&&o[Br];if(s||(n[e]=void 0),o)if(t&&Me(`Proxy of JS ${typeof o} with JSHandle ${e} was still alive. ${s?"keeping":"disposing"}.`),s)r=!0;else{const t=st.getPromiseController(o);t&&t.reject(new Error("WebWorker which is origin of the Task is being terminated.")),"function"==typeof o.dispose&&o.dispose(),o[Rr]===e&&(o[Rr]=void 0),i++}};for(let e=0;en.resolve(e))).catch((e=>n.reject(e))),t}const Gr=Symbol.for("wasm promise_holder");class Jr extends ManagedObject{constructor(e,t,n,r){super(),this.promise=e,this.gc_handle=t,this.promiseHolderPtr=n,this.res_converter=r,this.isResolved=!1,this.isPosted=!1,this.isPostponed=!1,this.data=null,this.reason=void 0}setIsResolving(){return!0}resolve(e){st.is_runtime_running()?(this.isResolved&&ut(!1,"resolve could be called only once"),this.isDisposed&&ut(!1,"resolve is already disposed."),this.isResolved=!0,this.complete_task_wrapper(e,null)):st.diagnosticTracing&&De("This promise resolution can't be propagated to managed code, mono runtime already exited.")}reject(e){st.is_runtime_running()?(e||(e=new Error),this.isResolved&&ut(!1,"reject could be called only once"),this.isDisposed&&ut(!1,"resolve is already disposed."),e[Gr],this.isResolved=!0,this.complete_task_wrapper(null,e)):st.diagnosticTracing&&De("This promise rejection can't be propagated to managed code, mono runtime already exited.")}cancel(){if(st.is_runtime_running())if(this.isResolved&&ut(!1,"cancel could be called only once"),this.isDisposed&&ut(!1,"resolve is already disposed."),this.isPostponed)this.isResolved=!0,void 0!==this.reason?this.complete_task_wrapper(null,this.reason):this.complete_task_wrapper(this.data,null);else{const e=this.promise;st.assertIsControllablePromise(e);const t=st.getPromiseController(e),n=new Error("OperationCanceledException");n[Gr]=this,t.reject(n)}else st.diagnosticTracing&&De("This promise cancelation can't be propagated to managed code, mono runtime already exited.")}complete_task_wrapper(e,t){try{this.isPosted&&ut(!1,"Promise is already posted to managed."),this.isPosted=!0,Fr(this,this.gc_handle,!0),function(e,t,n,r){st.assert_runtime_running();const o=Xe.stackSave();try{const o=xn(5),s=In(o,2);Mn(s,14),Xn(s,e);const a=In(o,3);if(t)ho(a,t);else{Mn(a,0);const e=In(o,4);r||ut(!1,"res_converter missing"),r(e,n)}hn(ot.ioThreadTID,mn.CompleteTask,o)}finally{Xe.stackRestore(o)}}(this.gc_handle,t,e,this.res_converter||bo)}catch(e){try{st.mono_exit(1,e)}catch(e){}}}}const Xr="For more information see https://aka.ms/dotnet-wasm-jsinterop";function Qr(e,t,n){if(0===t||1===t||2===t||26===t)return;let r,o,s,a;o=Ft(Rn(e)),s=Ft(Bn(e)),a=Ft(Nn(e));const i=Ln(e);r=Yr(i),19===t&&(t=i);const c=Yr(t),l=Rn(e),p=n*Un;return(e,t)=>{c(e+p,t,l,r,o,s,a)}}function Yr(e){if(0===e||1===e)return;const t=wn.get(e);return t&&"function"==typeof t||ut(!1,`ERR30: Unknown converter for type ${e}`),t}function Zr(e,t){null==t?Mn(e,0):(Mn(e,3),Vn(e,t))}function Kr(e,t){null==t?Mn(e,0):(Mn(e,4),function(e,t){e||ut(!1,"Null arg"),g(e,t)}(e,t))}function eo(e,t){null==t?Mn(e,0):(Mn(e,5),function(e,t){e||ut(!1,"Null arg"),b(e,t)}(e,t))}function to(e,t){null==t?Mn(e,0):(Mn(e,6),function(e,t){e||ut(!1,"Null arg"),S(e,t)}(e,t))}function no(e,t){null==t?Mn(e,0):(Mn(e,7),function(e,t){e||ut(!1,"Null arg"),v(e,t)}(e,t))}function ro(e,t){null==t?Mn(e,0):(Mn(e,8),function(e,t){if(e||ut(!1,"Null arg"),!Number.isSafeInteger(t))throw new Error(`Assert failed: Value is not an integer: ${t} (${typeof t})`);A(e,t)}(e,t))}function oo(e,t){null==t?Mn(e,0):(Mn(e,9),function(e,t){e||ut(!1,"Null arg"),x(e,t)}(e,t))}function so(e,t){null==t?Mn(e,0):(Mn(e,10),Wn(e,t))}function ao(e,t){null==t?Mn(e,0):(Mn(e,11),function(e,t){e||ut(!1,"Null arg"),I(e,t)}(e,t))}function io(e,t){null==t?Mn(e,0):(Mn(e,12),zn(e,t))}function co(e,t){if(null==t)Mn(e,0);else{if(!(t instanceof Date))throw new Error("Assert failed: Value is not a Date");Mn(e,17),Hn(e,t)}}function lo(e,t){if(null==t)Mn(e,0);else{if(!(t instanceof Date))throw new Error("Assert failed: Value is not a Date");Mn(e,18),Hn(e,t)}}function po(e,t){if(null==t)Mn(e,0);else{if(Mn(e,15),"string"!=typeof t)throw new Error("Assert failed: Value is not a String");uo(e,t)}}function uo(e,t){{const n=Qn(e);try{!function(e,t){if(t.clear(),null!==e)if("symbol"==typeof e)Re(e,t);else{if("string"!=typeof e)throw new Error("Expected string argument, got "+typeof e);if(0===e.length)Re(e,t);else{if(e.length<=256){const n=_e.get(e);if(n)return void t.set(n)}Be(e,t)}}}(t,n)}finally{n.release()}}}function fo(e){Mn(e,0)}function _o(e,t,r,o,s,a,i){if(null==t)return void Mn(e,0);if(!(t&&t instanceof Function))throw new Error("Assert failed: Value is not a Function");const c=function(e){const r=In(e,0),l=In(e,1),p=In(e,2),u=In(e,3),d=In(e,4),f=ot.isPendingSynchronousCall;try{let e,r,f;n&&c.isDisposed,s&&(e=s(p)),a&&(r=a(u)),i&&(f=i(d)),ot.isPendingSynchronousCall=!0;const _=t(e,r,f);o&&o(l,_)}catch(e){ho(r,e)}finally{ot.isPendingSynchronousCall=f}};c[Sn]=!0,c.isDisposed=!1,c.dispose=()=>{c.isDisposed=!0},Gn(e,Cr(c)),Mn(e,25)}function mo(e,t,n,r){const o=30==Dn(e);if(null==t)return void Mn(e,0);if(!Wr(t))throw new Error("Assert failed: Value is not a Promise");const s=o?Jn(e):xr.length?xr.pop():Ir--;o||(Xn(e,s),Mn(e,20));const a=new Jr(t,s,0,r);Dr(a,s),t.then((e=>a.resolve(e)),(e=>a.reject(e)))}function ho(e,t){if(null==t)Mn(e,0);else if(t instanceof ManagedError)Mn(e,16),Xn(e,Mr(t));else{if("object"!=typeof t&&"string"!=typeof t)throw new Error("Assert failed: Value is not an Error "+typeof t);Mn(e,27),uo(e,t.toString());const n=t[Rr];Gn(e,n||Cr(t))}}function go(e,t){if(null==t)Mn(e,0);else{if(void 0!==t[Lr])throw new Error(`Assert failed: JSObject proxy of ManagedObject proxy is not supported. ${Xr}`);if("function"!=typeof t&&"object"!=typeof t)throw new Error(`Assert failed: JSObject proxy of ${typeof t} is not supported`);Mn(e,13),Gn(e,Cr(t))}}function bo(e,t){if(null==t)Mn(e,0);else{const n=t[Lr],r=typeof t;if(void 0===n)if("string"===r||"symbol"===r)Mn(e,15),uo(e,t);else if("number"===r)Mn(e,10),Wn(e,t);else{if("bigint"===r)throw new Error("NotImplementedException: bigint");if("boolean"===r)Mn(e,3),Vn(e,t);else if(t instanceof Date)Mn(e,17),Hn(e,t);else if(t instanceof Error)ho(e,t);else if(t instanceof Uint8Array)wo(e,t,4);else if(t instanceof Float64Array)wo(e,t,10);else if(t instanceof Int32Array)wo(e,t,7);else if(Array.isArray(t))wo(e,t,14);else{if(t instanceof Int16Array||t instanceof Int8Array||t instanceof Uint8ClampedArray||t instanceof Uint16Array||t instanceof Uint32Array||t instanceof Float32Array)throw new Error("NotImplementedException: TypedArray");if(Wr(t))mo(e,t);else{if(t instanceof Span)throw new Error("NotImplementedException: Span");if("object"!=r)throw new Error(`JSObject proxy is not supported for ${r} ${t}`);{const n=Cr(t);Mn(e,13),Gn(e,n)}}}}else{if(Mr(t),t instanceof ArraySegment)throw new Error("NotImplementedException: ArraySegment. "+Xr);if(t instanceof ManagedError)Mn(e,16),Xn(e,n);else{if(!(t instanceof ManagedObject))throw new Error("NotImplementedException "+r+". "+Xr);Mn(e,14),Xn(e,n)}}}}function yo(e,t,n){n||ut(!1,"Expected valid element_type parameter"),wo(e,t,n)}function wo(e,t,n){if(null==t)Mn(e,0);else{const r=Kn(n);-1==r&&ut(!1,`Element type ${n} not supported`);const s=t.length,a=r*s,i=Xe._malloc(a);if(15==n){if(!Array.isArray(t))throw new Error("Assert failed: Value is not an Array");_(i,a),o.mono_wasm_register_root(i,a,"marshal_array_to_cs");for(let e=0;e>2,(i>>2)+s).set(t)}else{if(10!=n)throw new Error("not implemented");if(!(Array.isArray(t)||t instanceof Float64Array))throw new Error("Assert failed: Value is not an Array or Float64Array");te().subarray(i>>3,(i>>3)+s).set(t)}zn(e,i),Mn(e,21),function(e,t){e||ut(!1,"Null arg"),g(e+13,t)}(e,n),Zn(e,t.length)}}function ko(e,t,n){if(n||ut(!1,"Expected valid element_type parameter"),t.isDisposed)throw new Error("Assert failed: ObjectDisposedException");vo(n,t._viewType),Mn(e,23),zn(e,t._pointer),Zn(e,t.length)}function So(e,t,n){n||ut(!1,"Expected valid element_type parameter");const r=Mr(t);r||ut(!1,"Only roundtrip of ArraySegment instance created by C#"),vo(n,t._viewType),Mn(e,22),zn(e,t._pointer),Zn(e,t.length),Xn(e,r)}function vo(e,t){if(4==e){if(0!=t)throw new Error("Assert failed: Expected MemoryViewType.Byte")}else if(7==e){if(1!=t)throw new Error("Assert failed: Expected MemoryViewType.Int32")}else{if(10!=e)throw new Error(`NotImplementedException ${e} `);if(2!=t)throw new Error("Assert failed: Expected MemoryViewType.Double")}}const Uo={now:function(){return Date.now()}};function Eo(e){void 0===globalThis.performance&&(globalThis.performance=Uo),e.require=Qe.require,e.scriptDirectory=st.scriptDirectory,Xe.locateFile===Xe.__locateFile&&(Xe.locateFile=st.locateFile),e.fetch=st.fetch_like,e.ENVIRONMENT_IS_WORKER=et}function To(){if("function"!=typeof globalThis.fetch||"function"!=typeof globalThis.AbortController)throw new Error(Ye?"Please install `node-fetch` and `node-abort-controller` npm packages to enable HTTP client support. See also https://aka.ms/dotnet-wasm-features":"This browser doesn't support fetch API. Please use a modern browser. See also https://aka.ms/dotnet-wasm-features")}let xo,Io;function Ao(){if(void 0!==xo)return xo;if("undefined"!=typeof Request&&"body"in Request.prototype&&"function"==typeof ReadableStream&&"function"==typeof TransformStream){let e=!1;const t=new Request("",{body:new ReadableStream,method:"POST",get duplex(){return e=!0,"half"}}).headers.has("Content-Type");xo=e&&!t}else xo=!1;return xo}function jo(){return void 0!==Io||(Io="undefined"!=typeof Response&&"body"in Response.prototype&&"function"==typeof ReadableStream),Io}function $o(){return To(),dr(),{abortController:new AbortController}}function Lo(e){e.catch((e=>{e&&"AbortError"!==e&&"AbortError"!==e.name&&De("http muted: "+e)}))}function Ro(e){try{e.isAborted||(e.streamWriter&&(Lo(e.streamWriter.abort()),e.isAborted=!0),e.streamReader&&(Lo(e.streamReader.cancel()),e.isAborted=!0)),e.isAborted||e.abortController.signal.aborted||e.abortController.abort("AbortError")}catch(e){}}function Bo(e,t,n){n>0||ut(!1,"expected bufferLength > 0");const r=new Span(t,n,0).slice();return qr((async()=>{e.streamWriter||ut(!1,"expected streamWriter"),e.responsePromise||ut(!1,"expected fetch promise");try{await e.streamWriter.ready,await e.streamWriter.write(r)}catch(e){throw new Error("BrowserHttpWriteStream.Rejected")}}))}function No(e){return e||ut(!1,"expected controller"),qr((async()=>{e.streamWriter||ut(!1,"expected streamWriter"),e.responsePromise||ut(!1,"expected fetch promise");try{await e.streamWriter.ready,await e.streamWriter.close()}catch(e){throw new Error("BrowserHttpWriteStream.Rejected")}}))}function Co(e,t,n,r,o,s){const a=new TransformStream;return e.streamWriter=a.writable.getWriter(),Lo(e.streamWriter.closed),Lo(e.streamWriter.ready),Do(e,t,n,r,o,s,a.readable)}function Oo(e,t,n,r,o,s,a,i){return Do(e,t,n,r,o,s,new Span(a,i,0).slice())}function Do(e,t,n,r,o,s,a){To(),dr(),t&&"string"==typeof t||ut(!1,"expected url string"),n&&r&&Array.isArray(n)&&Array.isArray(r)&&n.length===r.length||ut(!1,"expected headerNames and headerValues arrays"),o&&s&&Array.isArray(o)&&Array.isArray(s)&&o.length===s.length||ut(!1,"expected headerNames and headerValues arrays");const i=new Headers;for(let e=0;est.fetch_like(t,c).then((t=>(e.response=t,null))))),e.responsePromise.then((()=>{if(e.response||ut(!1,"expected response"),e.responseHeaderNames=[],e.responseHeaderValues=[],e.response.headers&&e.response.headers.entries){const t=e.response.headers.entries();for(const n of t)e.responseHeaderNames.push(n[0]),e.responseHeaderValues.push(n[1])}})).catch((()=>{})),e.responsePromise}function Fo(e){var t;return null===(t=e.response)||void 0===t?void 0:t.type}function Mo(e){var t,n;return null!==(n=null===(t=e.response)||void 0===t?void 0:t.status)&&void 0!==n?n:0}function Po(e){return e.responseHeaderNames||ut(!1,"expected responseHeaderNames"),e.responseHeaderNames}function Vo(e){return e.responseHeaderValues||ut(!1,"expected responseHeaderValues"),e.responseHeaderValues}function zo(e){return qr((async()=>{const t=await e.response.arrayBuffer();return e.responseBuffer=t,e.currentBufferOffset=0,t.byteLength}))}function Ho(e,t){if(e||ut(!1,"expected controller"),e.responseBuffer||ut(!1,"expected resoved arrayBuffer"),null==e.currentBufferOffset&&ut(!1,"expected currentBufferOffset"),e.currentBufferOffset==e.responseBuffer.byteLength)return 0;const n=new Uint8Array(e.responseBuffer,e.currentBufferOffset);t.set(n,0);const r=Math.min(t.byteLength,n.byteLength);return e.currentBufferOffset+=r,r}function Wo(e,t,n){const r=new Span(t,n,0);return qr((async()=>{if(await e.responsePromise,e.response||ut(!1,"expected response"),!e.response.body)return 0;if(e.streamReader||(e.streamReader=e.response.body.getReader(),Lo(e.streamReader.closed)),e.currentStreamReaderChunk&&void 0!==e.currentBufferOffset||(e.currentStreamReaderChunk=await e.streamReader.read(),e.currentBufferOffset=0),e.currentStreamReaderChunk.done){if(e.isAborted)throw new Error("OperationCanceledException");return 0}const t=e.currentStreamReaderChunk.value.byteLength-e.currentBufferOffset;t>0||ut(!1,"expected remaining_source to be greater than 0");const n=Math.min(t,r.byteLength),o=e.currentStreamReaderChunk.value.subarray(e.currentBufferOffset,e.currentBufferOffset+n);return r.set(o,0),e.currentBufferOffset+=n,t==n&&(e.currentStreamReaderChunk=void 0),n}))}let qo,Go=0,Jo=0;function Xo(){if(!st.isChromium)return;const e=(new Date).valueOf(),t=e+36e4;for(let n=Math.max(e+1e3,Go);n0;){if(--Jo,!st.is_runtime_running())return;o.mono_background_exec()}}catch(e){st.mono_exit(1,e)}}function mono_wasm_schedule_timer_tick(){if(Xe.maybeExit(),st.is_runtime_running()){qo=void 0;try{o.mono_wasm_execute_timer(),Jo++}catch(e){st.mono_exit(1,e)}}}class Zo{constructor(){this.queue=[],this.offset=0}getLength(){return this.queue.length-this.offset}isEmpty(){return 0==this.queue.length}enqueue(e){this.queue.push(e)}dequeue(){if(0===this.queue.length)return;const e=this.queue[this.offset];return this.queue[this.offset]=null,2*++this.offset>=this.queue.length&&(this.queue=this.queue.slice(this.offset),this.offset=0),e}peek(){return this.queue.length>0?this.queue[this.offset]:void 0}drain(e){for(;this.getLength();)e(this.dequeue())}}const Ko=Symbol.for("wasm ws_pending_send_buffer"),es=Symbol.for("wasm ws_pending_send_buffer_offset"),ts=Symbol.for("wasm ws_pending_send_buffer_type"),ns=Symbol.for("wasm ws_pending_receive_event_queue"),rs=Symbol.for("wasm ws_pending_receive_promise_queue"),os=Symbol.for("wasm ws_pending_open_promise"),ss=Symbol.for("wasm wasm_ws_pending_open_promise_used"),as=Symbol.for("wasm wasm_ws_pending_error"),is=Symbol.for("wasm ws_pending_close_promises"),cs=Symbol.for("wasm ws_pending_send_promises"),ls=Symbol.for("wasm ws_is_aborted"),ps=Symbol.for("wasm wasm_ws_close_sent"),us=Symbol.for("wasm wasm_ws_close_received"),ds=Symbol.for("wasm ws_receive_status_ptr"),fs=65536,_s=new Uint8Array;function ms(e){var t,n;return e.readyState!=WebSocket.CLOSED?null!==(t=e.readyState)&&void 0!==t?t:-1:0==e[ns].getLength()?null!==(n=e.readyState)&&void 0!==n?n:-1:WebSocket.OPEN}function hs(e,t,n){let r;!function(){if(nt)throw new Error("WebSockets are not supported in shell JS engine.");if("function"!=typeof globalThis.WebSocket)throw new Error(Ye?"Please install `ws` npm package to enable networking support. See also https://aka.ms/dotnet-wasm-features":"This browser doesn't support WebSocket API. Please use a modern browser. See also https://aka.ms/dotnet-wasm-features")}(),dr(),e&&"string"==typeof e||ut(!1,"ERR12: Invalid uri "+typeof e);try{r=new globalThis.WebSocket(e,t||void 0)}catch(e){throw Me("WebSocket error in ws_wasm_create: "+e.toString()),e}const{promise_control:o}=pt();r[ns]=new Zo,r[rs]=new Zo,r[os]=o,r[cs]=[],r[is]=[],r[ds]=n,r.binaryType="arraybuffer";const s=()=>{try{if(r[ls])return;if(!st.is_runtime_running())return;o.resolve(r),Xo()}catch(e){Me("failed to propagate WebSocket open event: "+e.toString())}},a=e=>{try{if(r[ls])return;if(!st.is_runtime_running())return;!function(e,t){const n=e[ns],r=e[rs];if("string"==typeof t.data)n.enqueue({type:0,data:Te(t.data),offset:0});else{if("ArrayBuffer"!==t.data.constructor.name)throw new Error("ERR19: WebSocket receive expected ArrayBuffer");n.enqueue({type:1,data:new Uint8Array(t.data),offset:0})}if(r.getLength()&&n.getLength()>1)throw new Error("ERR21: Invalid WS state");for(;r.getLength()&&n.getLength();){const t=r.dequeue();vs(e,n,t.buffer_ptr,t.buffer_length),t.resolve()}Xo()}(r,e),Xo()}catch(e){Me("failed to propagate WebSocket message event: "+e.toString())}},i=e=>{try{if(r.removeEventListener("message",a),r[ls])return;if(!st.is_runtime_running())return;r[us]=!0,r.close_status=e.code,r.close_status_description=e.reason,r[ss]&&o.reject(new Error(e.reason));for(const e of r[is])e.resolve();r[rs].drain((e=>{v(n,0),v(n+4,2),v(n+8,1),e.resolve()}))}catch(e){Me("failed to propagate WebSocket close event: "+e.toString())}},c=e=>{try{if(r[ls])return;if(!st.is_runtime_running())return;r.removeEventListener("message",a);const t=e.message?"WebSocket error: "+e.message:"WebSocket error";Me(t),r[as]=t,Ss(r,new Error(t))}catch(e){Me("failed to propagate WebSocket error event: "+e.toString())}};return r.addEventListener("message",a),r.addEventListener("open",s,{once:!0}),r.addEventListener("close",i,{once:!0}),r.addEventListener("error",c,{once:!0}),r.dispose=()=>{r.removeEventListener("message",a),r.removeEventListener("open",s),r.removeEventListener("close",i),r.removeEventListener("error",c),ks(r)},r}function gs(e){if(e||ut(!1,"ERR17: expected ws instance"),e[as])return Us(e[as]);const t=e[os];return e[ss]=!0,t.promise}function bs(e,t,n,r,o){if(e||ut(!1,"ERR17: expected ws instance"),e[as])return Us(e[as]);if(e[ls]||e[ps])return Us("InvalidState: The WebSocket is not connected.");if(e.readyState==WebSocket.CLOSED)return null;const s=function(e,t,n,r){let o=e[Ko],s=0;const a=t.byteLength;if(o){if(s=e[es],n=e[ts],0!==a){if(s+a>o.length){const n=new Uint8Array(1.5*(s+a+50));n.set(o,0),n.subarray(s).set(t),e[Ko]=o=n}else o.subarray(s).set(t);s+=a,e[es]=s}}else r?0!==a&&(o=t,s=a):(0!==a&&(o=t.slice(),s=a,e[es]=s,e[Ko]=o),e[ts]=n);return r?0==s||null==o?_s:0===n?function(e){return void 0===ye?Xe.UTF8ArrayToString(e,0,e.byteLength):ye.decode(e)}(Ne(o,0,s)):o.subarray(0,s):null}(e,new Uint8Array(Y().buffer,t,n),r,o);return o&&s?function(e,t){if(e.send(t),e[Ko]=null,e.bufferedAmount{try{if(0===e.bufferedAmount)r.resolve();else{const t=e.readyState;if(t!=WebSocket.OPEN&&t!=WebSocket.CLOSING)r.reject(new Error(`InvalidState: ${t} The WebSocket is not connected.`));else if(!r.isDone)return globalThis.setTimeout(a,s),void(s=Math.min(1.5*s,1e3))}const t=o.indexOf(r);t>-1&&o.splice(t,1)}catch(e){Me("WebSocket error in web_socket_send_and_wait: "+e.toString()),r.reject(e)}};return globalThis.setTimeout(a,0),n}(e,s):null}function ys(e,t,n){if(e||ut(!1,"ERR18: expected ws instance"),e[as])return Us(e[as]);if(e[ls]){const t=e[ds];return v(t,0),v(t+4,2),v(t+8,1),null}const r=e[ns],o=e[rs];if(r.getLength())return 0!=o.getLength()&&ut(!1,"ERR20: Invalid WS state"),vs(e,r,t,n),null;if(e[us]){const t=e[ds];return v(t,0),v(t+4,2),v(t+8,1),null}const{promise:s,promise_control:a}=pt(),i=a;return i.buffer_ptr=t,i.buffer_length=n,o.enqueue(i),s}function ws(e,t,n,r){if(e||ut(!1,"ERR19: expected ws instance"),e[ls]||e[ps]||e.readyState==WebSocket.CLOSED)return null;if(e[as])return Us(e[as]);if(e[ps]=!0,r){const{promise:r,promise_control:o}=pt();return e[is].push(o),"string"==typeof n?e.close(t,n):e.close(t),r}return"string"==typeof n?e.close(t,n):e.close(t),null}function ks(e){if(e||ut(!1,"ERR18: expected ws instance"),!e[ls]&&!e[ps]){e[ls]=!0,Ss(e,new Error("OperationCanceledException"));try{e.close(1e3,"Connection was aborted.")}catch(e){Me("WebSocket error in ws_wasm_abort: "+e.toString())}}}function Ss(e,t){const n=e[os],r=e[ss];n&&r&&n.reject(t);for(const n of e[is])n.reject(t);for(const n of e[cs])n.reject(t);e[rs].drain((e=>{e.reject(t)}))}function vs(e,t,n,r){const o=t.peek(),s=Math.min(r,o.data.length-o.offset);if(s>0){const e=o.data.subarray(o.offset,o.offset+s);new Uint8Array(Y().buffer,n,r).set(e,0),o.offset+=s}const a=o.data.length===o.offset?1:0;a&&t.dequeue();const i=e[ds];v(i,s),v(i+4,o.type),v(i+8,a)}function Us(e){return function(e){const{promise:t,promise_control:n}=pt();return e.then((e=>n.resolve(e))).catch((e=>n.reject(e))),t}(Promise.reject(new Error(e)))}function Es(e,t,n){st.diagnosticTracing&&De(`Loaded:${e.name} as ${e.behavior} size ${n.length} from ${t}`);const r=Bt(),s="string"==typeof e.virtualPath?e.virtualPath:e.name;let a=null;switch(e.behavior){case"dotnetwasm":case"js-module-threads":case"js-module-globalization":case"symbols":case"segmentation-rules":break;case"resource":case"assembly":case"pdb":st._loaded_files.push({url:t,file:s});case"heap":case"icu":a=function(e){const t=e.length+16;let n=Xe._sbrk(t);if(n<=0){if(n=Xe._sbrk(t),n<=0)throw Pe(`sbrk failed to allocate ${t} bytes, and failed upon retry.`),new Error("Out of memory");Me(`sbrk failed to allocate ${t} bytes, but succeeded upon retry!`)}return new Uint8Array(Y().buffer,n,e.length).set(e),n}(n);break;case"vfs":{const e=s.lastIndexOf("/");let t=e>0?s.substring(0,e):null,r=e>0?s.substring(e+1):s;r.startsWith("/")&&(r=r.substring(1)),t?(t.startsWith("/")||(t="/"+t),De(`Creating directory '${t}'`),Xe.FS_createPath("/",t,!0,!0)):t="/",st.diagnosticTracing&&De(`Creating file '${r}' in directory '${t}'`),Xe.FS_createDataFile(t,r,n,!0,!0,!0);break}default:throw new Error(`Unrecognized asset behavior:${e.behavior}, for asset ${e.name}`)}if("assembly"===e.behavior){if(!o.mono_wasm_add_assembly(s,a,n.length)){const e=st._loaded_files.findIndex((e=>e.file==s));st._loaded_files.splice(e,1)}}else"pdb"===e.behavior?o.mono_wasm_add_assembly(s,a,n.length):"icu"===e.behavior?function(e){if(!o.mono_wasm_load_icu_data(e))throw new Error("Failed to load ICU data")}(a):"resource"===e.behavior&&o.mono_wasm_add_satellite_assembly(s,e.culture||"",a,n.length);Nt(r,"mono.instantiateAsset:",e.name),++st.actual_instantiated_assets_count}async function Ts(e){try{const n=await e.pendingDownloadInternal.response;t=await n.text(),ze&&ut(!1,"Another symbol map was already loaded"),ze=t,st.diagnosticTracing&&De(`Deferred loading of ${t.length}ch symbol map`)}catch(t){Fe(`Error loading symbol file ${e.name}: ${JSON.stringify(t)}`)}var t}async function xs(e){try{const t=await e.pendingDownloadInternal.response,n=await t.json();at.setSegmentationRulesFromJson(n)}catch(t){Fe(`Error loading static json asset ${e.name}: ${JSON.stringify(t)}`)}}function Is(){return st.loadedFiles}const As={};function js(e){let t=As[e];if("string"!=typeof t){const n=o.mono_jiterp_get_opcode_info(e,0);As[e]=t=xe(n)}return t}const $s=2,Ls=64,Rs=64,Bs={};class Ns{constructor(e){this.locals=new Map,this.permanentFunctionTypeCount=0,this.permanentFunctionTypes={},this.permanentFunctionTypesByShape={},this.permanentFunctionTypesByIndex={},this.functionTypesByIndex={},this.permanentImportedFunctionCount=0,this.permanentImportedFunctions={},this.nextImportIndex=0,this.functions=[],this.estimatedExportBytes=0,this.frame=0,this.traceBuf=[],this.branchTargets=new Set,this.constantSlots=[],this.backBranchOffsets=[],this.callHandlerReturnAddresses=[],this.nextConstantSlot=0,this.backBranchTraceLevel=0,this.compressImportNames=!1,this.lockImports=!1,this._assignParameterIndices=e=>{let t=0;for(const n in e)this.locals.set(n,t),t++;return t},this.stack=[new Cs],this.clear(e),this.cfg=new Os(this),this.defineType("__cpp_exception",{ptr:127},64,!0)}clear(e){this.options=pa(),this.stackSize=1,this.inSection=!1,this.inFunction=!1,this.lockImports=!1,this.locals.clear(),this.functionTypeCount=this.permanentFunctionTypeCount,this.functionTypes=Object.create(this.permanentFunctionTypes),this.functionTypesByShape=Object.create(this.permanentFunctionTypesByShape),this.functionTypesByIndex=Object.create(this.permanentFunctionTypesByIndex),this.nextImportIndex=0,this.importedFunctionCount=0,this.importedFunctions=Object.create(this.permanentImportedFunctions);for(const e in this.importedFunctions)this.importedFunctions[e].index=void 0;this.functions.length=0,this.estimatedExportBytes=0,this.argumentCount=0,this.current.clear(),this.traceBuf.length=0,this.branchTargets.clear(),this.activeBlocks=0,this.nextConstantSlot=0,this.constantSlots.length=this.options.useConstants?e:0;for(let e=0;e=this.stack.length&&this.stack.push(new Cs),this.current.clear()}_pop(e){if(this.stackSize<=1)throw new Error("Stack empty");const t=this.current;return this.stackSize--,e?(this.appendULeb(t.size),t.copyTo(this.current),null):t.getArrayView(!1).slice(0,t.size)}setImportFunction(e,t){const n=this.importedFunctions[e];if(!n)throw new Error("No import named "+e);n.func=t}getExceptionTag(){const e=Xe.wasmExports.__cpp_exception;return void 0!==e&&(e instanceof WebAssembly.Tag||ut(!1,`expected __cpp_exception export from dotnet.wasm to be WebAssembly.Tag but was ${e}`)),e}getWasmImports(){const e=ot.getMemory();e instanceof WebAssembly.Memory||ut(!1,`expected heap import to be WebAssembly.Memory but was ${e}`);const t=this.getExceptionTag(),n={c:this.getConstants(),m:{h:e}};t&&(n.x={e:t});const r=this.getImportsToEmit();for(let e=0;e>>0||e>255)throw new Error(`Byte out of range: ${e}`);return this.current.appendU8(e)}appendSimd(e,t){return this.current.appendU8(253),0|e||0===e&&!0===t||ut(!1,"Expected non-v128_load simd opcode or allowLoad==true"),this.current.appendULeb(e)}appendAtomic(e,t){return this.current.appendU8(254),0|e||0===e&&!0===t||ut(!1,"Expected non-notify atomic opcode or allowNotify==true"),this.current.appendU8(e)}appendU32(e){return this.current.appendU32(e)}appendF32(e){return this.current.appendF32(e)}appendF64(e){return this.current.appendF64(e)}appendBoundaryValue(e,t){return this.current.appendBoundaryValue(e,t)}appendULeb(e){return this.current.appendULeb(e)}appendLeb(e){return this.current.appendLeb(e)}appendLebRef(e,t){return this.current.appendLebRef(e,t)}appendBytes(e){return this.current.appendBytes(e)}appendName(e){return this.current.appendName(e)}ret(e){this.ip_const(e),this.appendU8(15)}i32_const(e){this.appendU8(65),this.appendLeb(e)}ptr_const(e){let t=this.options.useConstants?this.constantSlots.indexOf(e):-1;this.options.useConstants&&t<0&&this.nextConstantSlot=0?(this.appendU8(35),this.appendLeb(t)):this.i32_const(e)}ip_const(e){this.appendU8(65),this.appendLeb(e-this.base)}i52_const(e){this.appendU8(66),this.appendLeb(e)}v128_const(e){if(0===e)this.local("v128_zero");else{if("object"!=typeof e)throw new Error("Expected v128_const arg to be 0 or a Uint8Array");{16!==e.byteLength&&ut(!1,"Expected v128_const arg to be 16 bytes in size");let t=!0;for(let n=0;n<16;n++)0!==e[n]&&(t=!1);t?this.local("v128_zero"):(this.appendSimd(12),this.appendBytes(e))}}}defineType(e,t,n,r){if(this.functionTypes[e])throw new Error(`Function type ${e} already defined`);if(r&&this.functionTypeCount>this.permanentFunctionTypeCount)throw new Error("New permanent function types cannot be defined after non-permanent ones");let o="";for(const e in t)o+=t[e]+",";o+=n;let s=this.functionTypesByShape[o];"number"!=typeof s&&(s=this.functionTypeCount++,r?(this.permanentFunctionTypeCount++,this.permanentFunctionTypesByShape[o]=s,this.permanentFunctionTypesByIndex[s]=[t,Object.values(t).length,n]):(this.functionTypesByShape[o]=s,this.functionTypesByIndex[s]=[t,Object.values(t).length,n]));const a=[s,t,n,`(${JSON.stringify(t)}) -> ${n}`,r];return r?this.permanentFunctionTypes[e]=a:this.functionTypes[e]=a,s}generateTypeSection(){this.beginSection(1),this.appendULeb(this.functionTypeCount);for(let e=0;ee.index-t.index)),e}_generateImportSection(e){const t=this.getImportsToEmit();if(this.lockImports=!0,!1!==e)throw new Error("function table imports are disabled");const n=void 0!==this.getExceptionTag();this.beginSection(2),this.appendULeb(1+(n?1:0)+t.length+this.constantSlots.length+(!1!==e?1:0));for(let e=0;e0)throw new Error("New permanent imports cannot be defined after any indexes have been assigned");const s=this.functionTypes[n];if(!s)throw new Error("No function type named "+n);if(r&&!s[4])throw new Error("A permanent import must have a permanent function type");const a=s[0],i=r?this.permanentImportedFunctions:this.importedFunctions;if("number"==typeof o&&(o=zs().get(o)),"function"!=typeof o&&void 0!==o)throw new Error(`Value passed for imported function ${t} was not a function or valid function pointer or undefined`);return i[t]={index:void 0,typeIndex:a,module:e,name:t,func:o}}markImportAsUsed(e){const t=this.importedFunctions[e];if(!t)throw new Error("No imported function named "+e);"number"!=typeof t.index&&(t.index=this.importedFunctionCount++)}getTypeIndex(e){const t=this.functionTypes[e];if(!t)throw new Error("No type named "+e);return t[0]}defineFunction(e,t){const n={index:this.functions.length,name:e.name,typeName:e.type,typeIndex:this.getTypeIndex(e.type),export:e.export,locals:e.locals,generator:t,error:null,blob:null};return this.functions.push(n),n.export&&(this.estimatedExportBytes+=n.name.length+8),n}emitImportsAndFunctions(e){let t=0;for(let e=0;e0)throw new Error(`${this.activeBlocks} unclosed block(s) at end of function`);const t=this._pop(e);return this.inFunction=!1,t}block(e,t){const n=this.appendU8(t||2);return e?this.appendU8(e):this.appendU8(64),this.activeBlocks++,n}endBlock(){if(this.activeBlocks<=0)throw new Error("No blocks active");this.activeBlocks--,this.appendU8(11)}arg(e,t){const n="string"==typeof e?this.locals.has(e)?this.locals.get(e):void 0:e;if("number"!=typeof n)throw new Error("No local named "+e);t&&this.appendU8(t),this.appendULeb(n)}local(e,t){const n="string"==typeof e?this.locals.has(e)?this.locals.get(e):void 0:e+this.argumentCount;if("number"!=typeof n)throw new Error("No local named "+e);t?this.appendU8(t):this.appendU8(32),this.appendULeb(n)}appendMemarg(e,t){this.appendULeb(t),this.appendULeb(e)}lea(e,t){"string"==typeof e?this.local(e):this.i32_const(e),this.i32_const(t),this.appendU8(106)}getArrayView(e){if(this.stackSize>1)throw new Error("Jiterpreter block stack not empty");return this.stack[0].getArrayView(e)}getConstants(){const e={};for(let t=0;t=this.capacity)throw new Error("Buffer full");const t=this.size;return Y()[this.buffer+this.size++]=e,t}appendU32(e){const t=this.size;return o.mono_jiterp_write_number_unaligned(this.buffer+this.size,e,0),this.size+=4,t}appendI32(e){const t=this.size;return o.mono_jiterp_write_number_unaligned(this.buffer+this.size,e,1),this.size+=4,t}appendF32(e){const t=this.size;return o.mono_jiterp_write_number_unaligned(this.buffer+this.size,e,2),this.size+=4,t}appendF64(e){const t=this.size;return o.mono_jiterp_write_number_unaligned(this.buffer+this.size,e,3),this.size+=8,t}appendBoundaryValue(e,t){if(this.size+8>=this.capacity)throw new Error("Buffer full");const n=o.mono_jiterp_encode_leb_signed_boundary(this.buffer+this.size,e,t);if(n<1)throw new Error(`Failed to encode ${e} bit boundary value with sign ${t}`);return this.size+=n,n}appendULeb(e){if("number"!=typeof e&&ut(!1,`appendULeb expected number but got ${e}`),e>=0||ut(!1,"cannot pass negative value to appendULeb"),e<127){if(this.size+1>=this.capacity)throw new Error("Buffer full");return this.appendU8(e),1}if(this.size+8>=this.capacity)throw new Error("Buffer full");const t=o.mono_jiterp_encode_leb52(this.buffer+this.size,e,0);if(t<1)throw new Error(`Failed to encode value '${e}' as unsigned leb`);return this.size+=t,t}appendLeb(e){if("number"!=typeof e&&ut(!1,`appendLeb expected number but got ${e}`),this.size+8>=this.capacity)throw new Error("Buffer full");const t=o.mono_jiterp_encode_leb52(this.buffer+this.size,e,1);if(t<1)throw new Error(`Failed to encode value '${e}' as signed leb`);return this.size+=t,t}appendLebRef(e,t){if(this.size+8>=this.capacity)throw new Error("Buffer full");const n=o.mono_jiterp_encode_leb64_ref(this.buffer+this.size,e,t?1:0);if(n<1)throw new Error("Failed to encode value as leb");return this.size+=n,n}copyTo(e,t){"number"!=typeof t&&(t=this.size),Y().copyWithin(e.buffer+e.size,this.buffer,this.buffer+t),e.size+=t}appendBytes(e,t){const n=this.size,r=Y();return e.buffer===r.buffer?("number"!=typeof t&&(t=e.length),r.copyWithin(this.buffer+n,e.byteOffset,e.byteOffset+t),this.size+=t):("number"==typeof t&&(e=new Uint8Array(e.buffer,e.byteOffset,t)),this.getArrayView(!0).set(e,this.size),this.size+=e.length),n}appendName(e){let t=e.length,n=1===e.length?e.charCodeAt(0):-1;if(n>127&&(n=-1),t&&n<0)if(this.encoder)t=this.encoder.encodeInto(e,this.textBuf).written||0;else for(let n=0;n127)throw new Error("Out of range character and no TextEncoder available");this.textBuf[n]=t}this.appendULeb(t),n>=0?this.appendU8(n):t>1&&this.appendBytes(this.textBuf,t)}getArrayView(e){return new Uint8Array(Y().buffer,this.buffer,e?this.capacity:this.size)}}class Os{constructor(e){this.segments=[],this.backBranchTargets=null,this.lastSegmentEnd=0,this.overheadBytes=0,this.blockStack=[],this.backDispatchOffsets=[],this.dispatchTable=new Map,this.observedBackBranchTargets=new Set,this.trace=0,this.builder=e}initialize(e,t,n){this.segments.length=0,this.blockStack.length=0,this.startOfBody=e,this.backBranchTargets=t,this.base=this.builder.base,this.ip=this.lastSegmentStartIp=this.firstOpcodeIp=this.builder.base,this.lastSegmentEnd=0,this.overheadBytes=10,this.dispatchTable.clear(),this.observedBackBranchTargets.clear(),this.trace=n,this.backDispatchOffsets.length=0}entry(e){this.entryIp=e;const t=o.mono_jiterp_get_opcode_info(675,1);return this.firstOpcodeIp=e+2*t,this.appendBlob(),1!==this.segments.length&&ut(!1,"expected 1 segment"),"blob"!==this.segments[0].type&&ut(!1,"expected blob"),this.entryBlob=this.segments[0],this.segments.length=0,this.overheadBytes+=9,this.backBranchTargets&&(this.overheadBytes+=20,this.overheadBytes+=this.backBranchTargets.length),this.firstOpcodeIp}appendBlob(){this.builder.current.size!==this.lastSegmentEnd&&(this.segments.push({type:"blob",ip:this.lastSegmentStartIp,start:this.lastSegmentEnd,length:this.builder.current.size-this.lastSegmentEnd}),this.lastSegmentStartIp=this.ip,this.lastSegmentEnd=this.builder.current.size,this.overheadBytes+=2)}startBranchBlock(e,t){this.appendBlob(),this.segments.push({type:"branch-block-header",ip:e,isBackBranchTarget:t}),this.overheadBytes+=1}branch(e,t,n){t&&this.observedBackBranchTargets.add(e),this.appendBlob(),this.segments.push({type:"branch",from:this.ip,target:e,isBackward:t,branchType:n}),this.overheadBytes+=4,t&&(this.overheadBytes+=4)}emitBlob(e,t){const n=t.subarray(e.start,e.start+e.length);this.builder.appendBytes(n)}generate(){this.appendBlob();const e=this.builder.endFunction(!1);this.builder._push(),this.builder.base=this.base,this.emitBlob(this.entryBlob,e),this.backBranchTargets&&this.builder.block(64,3);for(let e=0;ee-t));for(let e=0;e0&&Fe("No back branch targets were reachable after filtering");else if(1===this.backDispatchOffsets.length)this.trace>0&&(this.backDispatchOffsets[0]===this.entryIp?Fe(`Exactly one back dispatch offset and it was the entry point 0x${this.entryIp.toString(16)}`):Fe(`Exactly one back dispatch offset and it was 0x${this.backDispatchOffsets[0].toString(16)}`)),this.builder.local("disp"),this.builder.appendU8(13),this.builder.appendULeb(this.blockStack.indexOf(this.backDispatchOffsets[0]));else{this.trace>0&&Fe(`${this.backDispatchOffsets.length} back branch offsets after filtering.`),this.builder.block(64),this.builder.block(64),this.builder.local("disp"),this.builder.appendU8(14),this.builder.appendULeb(this.backDispatchOffsets.length+1),this.builder.appendULeb(1);for(let e=0;e0&&this.blockStack.push(0)}this.trace>1&&Fe(`blockStack=${this.blockStack}`);for(let t=0;t1&&Fe(`backward br from ${n.from.toString(16)} to ${n.target.toString(16)}: disp=${t}`),o=!0):(this.trace>0&&Fe(`br from ${n.from.toString(16)} to ${n.target.toString(16)} failed: back branch target not in dispatch table`),r=-1)),r>=0||o){let e=0;switch(n.branchType){case 2:this.builder,n.from,void 0!==t&&(this.builder.i32_const(t),this.builder.local("disp",33)),this.builder.appendU8(12);break;case 3:this.builder.block(64,4),this.builder,n.from,void 0!==t&&(this.builder.i32_const(t),this.builder.local("disp",33)),this.builder.appendU8(12),e=1;break;case 0:void 0!==t&&(this.builder.i32_const(t),this.builder.local("disp",33)),this.builder.appendU8(12);break;case 1:void 0!==t?(this.builder.block(64,4),this.builder.i32_const(t),this.builder.local("disp",33),e=1,this.builder.appendU8(12)):this.builder.appendU8(13);break;default:throw new Error("Unimplemented branch type")}this.builder.appendULeb(e+r),e&&this.builder.endBlock(),this.trace>1&&Fe(`br from ${n.from.toString(16)} to ${n.target.toString(16)} breaking out ${e+r+1} level(s)`)}else{if(this.trace>0){const e=this.base;n.target>=e&&n.target1&&Fe(`br from ${n.from.toString(16)} to ${n.target.toString(16)} failed (outside of trace 0x${e.toString(16)} - 0x${this.exitIp.toString(16)})`)}const e=1===n.branchType||3===n.branchType;e&&this.builder.block(64,4),Ps(this.builder,n.target,4),e&&this.builder.endBlock()}break}default:throw new Error("unreachable")}}return this.backBranchTargets&&(this.blockStack.length<=1||ut(!1,"expected one or zero entries in the block stack at the end"),this.blockStack.length&&this.blockStack.shift(),this.builder.endBlock()),0!==this.blockStack.length&&ut(!1,`expected block stack to be empty at end of function but it was ${this.blockStack}`),this.builder.ip_const(this.exitIp),this.builder.appendU8(15),this.builder.appendU8(11),this.builder._pop(!1)}}let Ds;const Fs={},Ms=globalThis.performance&&globalThis.performance.now?globalThis.performance.now.bind(globalThis.performance):Date.now;function Ps(e,t,n){e.ip_const(t),e.options.countBailouts&&(e.i32_const(e.traceIndex),e.i32_const(n),e.callImport("bailout")),e.appendU8(15)}function Vs(e,t,n,r){e.local("cinfo"),e.block(64,4),e.local("cinfo"),e.local("disp"),e.appendU8(54),e.appendMemarg(Ys(19),0),n<=e.options.monitoringLongDistance+2&&(e.local("cinfo"),e.i32_const(n),e.appendU8(54),e.appendMemarg(Ys(20),0)),e.endBlock(),e.ip_const(t),e.options.countBailouts&&(e.i32_const(e.traceIndex),e.i32_const(r),e.callImport("bailout")),e.appendU8(15)}function zs(){if(Ds||(Ds=ot.getWasmIndirectFunctionTable()),!Ds)throw new Error("Module did not export the indirect function table");return Ds}function Hs(e,t){t||ut(!1,"Attempting to set null function into table");const n=o.mono_jiterp_allocate_table_entry(e);return n>0&&zs().set(n,t),n}function Ws(e,t,n,r,o){if(r<=0)return o&&e.appendU8(26),!0;if(r>=Ls)return!1;const s=o?"memop_dest":"pLocals";o&&e.local(s,33);let a=o?0:t;if(e.options.enableSimd){const t=16;for(;r>=t;)e.local(s),e.v128_const(0),e.appendSimd(11),e.appendMemarg(a,0),a+=t,r-=t}for(;r>=8;)e.local(s),e.i52_const(0),e.appendU8(55),e.appendMemarg(a,0),a+=8,r-=8;for(;r>=1;){e.local(s),e.i32_const(0);let t=r%4;switch(t){case 0:t=4,e.appendU8(54);break;case 1:e.appendU8(58);break;case 3:case 2:t=2,e.appendU8(59)}e.appendMemarg(a,0),a+=t,r-=t}return!0}function qs(e,t,n){Ws(e,0,0,n,!0)||(e.i32_const(t),e.i32_const(n),e.appendU8(252),e.appendU8(11),e.appendU8(0))}function Gs(e,t,n,r,o,s,a){if(r<=0)return o&&(e.appendU8(26),e.appendU8(26)),!0;if(r>=Rs)return!1;o?(s=s||"memop_dest",a=a||"memop_src",e.local(a,33),e.local(s,33)):s&&a||(s=a="pLocals");let i=o?0:t,c=o?0:n;if(e.options.enableSimd){const t=16;for(;r>=t;)e.local(s),e.local(a),e.appendSimd(0,!0),e.appendMemarg(c,0),e.appendSimd(11),e.appendMemarg(i,0),i+=t,c+=t,r-=t}for(;r>=8;)e.local(s),e.local(a),e.appendU8(41),e.appendMemarg(c,0),e.appendU8(55),e.appendMemarg(i,0),i+=8,c+=8,r-=8;for(;r>=1;){let t,n,o=r%4;switch(o){case 0:o=4,t=40,n=54;break;default:case 1:o=1,t=44,n=58;break;case 3:case 2:o=2,t=46,n=59}e.local(s),e.local(a),e.appendU8(t),e.appendMemarg(c,0),e.appendU8(n),e.appendMemarg(i,0),c+=o,i+=o,r-=o}return!0}function Js(e,t){return Gs(e,0,0,t,!0)||(e.i32_const(t),e.appendU8(252),e.appendU8(10),e.appendU8(0),e.appendU8(0)),!0}function Xs(){const e=la(5,1);e>=$s&&(Fe(`Disabling jiterpreter after ${e} failures`),ia({enableTraces:!1,enableInterpEntry:!1,enableJitCall:!1}))}const Qs={};function Ys(e){const t=Qs[e];return void 0===t?Qs[e]=o.mono_jiterp_get_member_offset(e):t}function Zs(e){const t=Xe.wasmExports[e];if("function"!=typeof t)throw new Error(`raw cwrap ${e} not found`);return t}const Ks={};function ea(e){let t=Ks[e];return"number"!=typeof t&&(t=Ks[e]=o.mono_jiterp_get_opcode_value_table_entry(e)),t}function ta(e,t){return[e,e,t]}let na;function ra(){if(!o.mono_wasm_is_zero_page_reserved())return!1;if(!0===na)return!1;const e=K();for(let t=0;t<8;t++)if(0!==e[t])return!1===na&&Pe(`Zero page optimizations are enabled but garbage appeared in memory at address ${4*t}: ${e[t]}`),na=!0,!1;return na=!1,!0}const oa={enableTraces:"jiterpreter-traces-enabled",enableInterpEntry:"jiterpreter-interp-entry-enabled",enableJitCall:"jiterpreter-jit-call-enabled",enableBackwardBranches:"jiterpreter-backward-branch-entries-enabled",enableCallResume:"jiterpreter-call-resume-enabled",enableWasmEh:"jiterpreter-wasm-eh-enabled",enableSimd:"jiterpreter-simd-enabled",enableAtomics:"jiterpreter-atomics-enabled",zeroPageOptimization:"jiterpreter-zero-page-optimization",cprop:"jiterpreter-constant-propagation",enableStats:"jiterpreter-stats-enabled",disableHeuristic:"jiterpreter-disable-heuristic",estimateHeat:"jiterpreter-estimate-heat",countBailouts:"jiterpreter-count-bailouts",dumpTraces:"jiterpreter-dump-traces",useConstants:"jiterpreter-use-constants",eliminateNullChecks:"jiterpreter-eliminate-null-checks",noExitBackwardBranches:"jiterpreter-backward-branches-enabled",directJitCalls:"jiterpreter-direct-jit-calls",minimumTraceValue:"jiterpreter-minimum-trace-value",minimumTraceHitCount:"jiterpreter-minimum-trace-hit-count",monitoringPeriod:"jiterpreter-trace-monitoring-period",monitoringShortDistance:"jiterpreter-trace-monitoring-short-distance",monitoringLongDistance:"jiterpreter-trace-monitoring-long-distance",monitoringMaxAveragePenalty:"jiterpreter-trace-monitoring-max-average-penalty",backBranchBoost:"jiterpreter-back-branch-boost",jitCallHitCount:"jiterpreter-jit-call-hit-count",jitCallFlushThreshold:"jiterpreter-jit-call-queue-flush-threshold",interpEntryHitCount:"jiterpreter-interp-entry-hit-count",interpEntryFlushThreshold:"jiterpreter-interp-entry-queue-flush-threshold",wasmBytesLimit:"jiterpreter-wasm-bytes-limit",tableSize:"jiterpreter-table-size",aotTableSize:"jiterpreter-aot-table-size"};let sa=-1,aa={};function ia(e){for(const t in e){const n=oa[t];if(!n){Pe(`Unrecognized jiterpreter option: ${t}`);continue}const r=e[t];"boolean"==typeof r?o.mono_jiterp_parse_option((r?"--":"--no-")+n):"number"==typeof r?o.mono_jiterp_parse_option(`--${n}=${r}`):Pe(`Jiterpreter option must be a boolean or a number but was ${typeof r} '${r}'`)}}function ca(e){return o.mono_jiterp_get_counter(e)}function la(e,t){return o.mono_jiterp_modify_counter(e,t)}function pa(){const e=o.mono_jiterp_get_options_version();return e!==sa&&(function(){aa={};for(const e in oa){const t=o.mono_jiterp_get_option_as_int(oa[e]);t>-2147483647?aa[e]=t:Fe(`Failed to retrieve value of option ${oa[e]}`)}}(),sa=e),aa}function ua(e,t,n,r){const s=zs(),a=t,i=a+n-1;return i= ${s.length}`),s.set(a,r),o.mono_jiterp_initialize_table(e,a,i),t+n}let da=!1;const fa=["Unknown","InterpreterTiering","NullCheck","VtableNotInitialized","Branch","BackwardBranch","ConditionalBranch","ConditionalBackwardBranch","ComplexBranch","ArrayLoadFailed","ArrayStoreFailed","StringOperationFailed","DivideByZero","Overflow","Return","Call","Throw","AllocFailed","SpanOperationFailed","CastFailed","SafepointBranchTaken","UnboxFailed","CallDelegate","Debugging","Icall","UnexpectedRetIp","LeaveCheck"],_a={2:["V128_I1_NEGATION","V128_I2_NEGATION","V128_I4_NEGATION","V128_ONES_COMPLEMENT","V128_U2_WIDEN_LOWER","V128_U2_WIDEN_UPPER","V128_I1_CREATE_SCALAR","V128_I2_CREATE_SCALAR","V128_I4_CREATE_SCALAR","V128_I8_CREATE_SCALAR","V128_I1_EXTRACT_MSB","V128_I2_EXTRACT_MSB","V128_I4_EXTRACT_MSB","V128_I8_EXTRACT_MSB","V128_I1_CREATE","V128_I2_CREATE","V128_I4_CREATE","V128_I8_CREATE","SplatX1","SplatX2","SplatX4","SplatX8","NegateD1","NegateD2","NegateD4","NegateD8","NegateR4","NegateR8","SqrtR4","SqrtR8","CeilingR4","CeilingR8","FloorR4","FloorR8","TruncateR4","TruncateR8","RoundToNearestR4","RoundToNearestR8","NotANY","AnyTrueANY","AllTrueD1","AllTrueD2","AllTrueD4","AllTrueD8","PopCountU1","BitmaskD1","BitmaskD2","BitmaskD4","BitmaskD8","AddPairwiseWideningI1","AddPairwiseWideningU1","AddPairwiseWideningI2","AddPairwiseWideningU2","AbsI1","AbsI2","AbsI4","AbsI8","AbsR4","AbsR8","ConvertToSingleI4","ConvertToSingleU4","ConvertToSingleR8","ConvertToDoubleLowerI4","ConvertToDoubleLowerU4","ConvertToDoubleLowerR4","ConvertToInt32SaturateR4","ConvertToUInt32SaturateR4","ConvertToInt32SaturateR8","ConvertToUInt32SaturateR8","SignExtendWideningLowerD1","SignExtendWideningLowerD2","SignExtendWideningLowerD4","SignExtendWideningUpperD1","SignExtendWideningUpperD2","SignExtendWideningUpperD4","ZeroExtendWideningLowerD1","ZeroExtendWideningLowerD2","ZeroExtendWideningLowerD4","ZeroExtendWideningUpperD1","ZeroExtendWideningUpperD2","ZeroExtendWideningUpperD4","LoadVector128ANY","LoadScalarVector128X4","LoadScalarVector128X8","LoadScalarAndSplatVector128X1","LoadScalarAndSplatVector128X2","LoadScalarAndSplatVector128X4","LoadScalarAndSplatVector128X8","LoadWideningVector128I1","LoadWideningVector128U1","LoadWideningVector128I2","LoadWideningVector128U2","LoadWideningVector128I4","LoadWideningVector128U4"],3:["V128_I1_ADD","V128_I2_ADD","V128_I4_ADD","V128_R4_ADD","V128_I1_SUB","V128_I2_SUB","V128_I4_SUB","V128_R4_SUB","V128_BITWISE_AND","V128_BITWISE_OR","V128_BITWISE_EQUALITY","V128_BITWISE_INEQUALITY","V128_R4_FLOAT_EQUALITY","V128_R8_FLOAT_EQUALITY","V128_EXCLUSIVE_OR","V128_I1_MULTIPLY","V128_I2_MULTIPLY","V128_I4_MULTIPLY","V128_R4_MULTIPLY","V128_R4_DIVISION","V128_I1_LEFT_SHIFT","V128_I2_LEFT_SHIFT","V128_I4_LEFT_SHIFT","V128_I8_LEFT_SHIFT","V128_I1_RIGHT_SHIFT","V128_I2_RIGHT_SHIFT","V128_I4_RIGHT_SHIFT","V128_I1_URIGHT_SHIFT","V128_I2_URIGHT_SHIFT","V128_I4_URIGHT_SHIFT","V128_I8_URIGHT_SHIFT","V128_U1_NARROW","V128_U1_GREATER_THAN","V128_I1_LESS_THAN","V128_U1_LESS_THAN","V128_I2_LESS_THAN","V128_I1_EQUALS","V128_I2_EQUALS","V128_I4_EQUALS","V128_R4_EQUALS","V128_I8_EQUALS","V128_I1_EQUALS_ANY","V128_I2_EQUALS_ANY","V128_I4_EQUALS_ANY","V128_I8_EQUALS_ANY","V128_AND_NOT","V128_U2_LESS_THAN_EQUAL","V128_I1_SHUFFLE","V128_I2_SHUFFLE","V128_I4_SHUFFLE","V128_I8_SHUFFLE","ExtractScalarI1","ExtractScalarU1","ExtractScalarI2","ExtractScalarU2","ExtractScalarD4","ExtractScalarD8","ExtractScalarR4","ExtractScalarR8","SwizzleD1","AddD1","AddD2","AddD4","AddD8","AddR4","AddR8","SubtractD1","SubtractD2","SubtractD4","SubtractD8","SubtractR4","SubtractR8","MultiplyD2","MultiplyD4","MultiplyD8","MultiplyR4","MultiplyR8","DivideR4","DivideR8","DotI2","ShiftLeftD1","ShiftLeftD2","ShiftLeftD4","ShiftLeftD8","ShiftRightArithmeticD1","ShiftRightArithmeticD2","ShiftRightArithmeticD4","ShiftRightArithmeticD8","ShiftRightLogicalD1","ShiftRightLogicalD2","ShiftRightLogicalD4","ShiftRightLogicalD8","AndANY","AndNotANY","OrANY","XorANY","CompareEqualD1","CompareEqualD2","CompareEqualD4","CompareEqualD8","CompareEqualR4","CompareEqualR8","CompareNotEqualD1","CompareNotEqualD2","CompareNotEqualD4","CompareNotEqualD8","CompareNotEqualR4","CompareNotEqualR8","CompareLessThanI1","CompareLessThanU1","CompareLessThanI2","CompareLessThanU2","CompareLessThanI4","CompareLessThanU4","CompareLessThanI8","CompareLessThanR4","CompareLessThanR8","CompareLessThanOrEqualI1","CompareLessThanOrEqualU1","CompareLessThanOrEqualI2","CompareLessThanOrEqualU2","CompareLessThanOrEqualI4","CompareLessThanOrEqualU4","CompareLessThanOrEqualI8","CompareLessThanOrEqualR4","CompareLessThanOrEqualR8","CompareGreaterThanI1","CompareGreaterThanU1","CompareGreaterThanI2","CompareGreaterThanU2","CompareGreaterThanI4","CompareGreaterThanU4","CompareGreaterThanI8","CompareGreaterThanR4","CompareGreaterThanR8","CompareGreaterThanOrEqualI1","CompareGreaterThanOrEqualU1","CompareGreaterThanOrEqualI2","CompareGreaterThanOrEqualU2","CompareGreaterThanOrEqualI4","CompareGreaterThanOrEqualU4","CompareGreaterThanOrEqualI8","CompareGreaterThanOrEqualR4","CompareGreaterThanOrEqualR8","ConvertNarrowingSaturateSignedI2","ConvertNarrowingSaturateSignedI4","ConvertNarrowingSaturateUnsignedI2","ConvertNarrowingSaturateUnsignedI4","MultiplyWideningLowerI1","MultiplyWideningLowerI2","MultiplyWideningLowerI4","MultiplyWideningLowerU1","MultiplyWideningLowerU2","MultiplyWideningLowerU4","MultiplyWideningUpperI1","MultiplyWideningUpperI2","MultiplyWideningUpperI4","MultiplyWideningUpperU1","MultiplyWideningUpperU2","MultiplyWideningUpperU4","AddSaturateI1","AddSaturateU1","AddSaturateI2","AddSaturateU2","SubtractSaturateI1","SubtractSaturateU1","SubtractSaturateI2","SubtractSaturateU2","MultiplyRoundedSaturateQ15I2","MinI1","MinI2","MinI4","MinU1","MinU2","MinU4","MaxI1","MaxI2","MaxI4","MaxU1","MaxU2","MaxU4","AverageRoundedU1","AverageRoundedU2","MinR4","MinR8","MaxR4","MaxR8","PseudoMinR4","PseudoMinR8","PseudoMaxR4","PseudoMaxR8","StoreANY"],4:["V128_CONDITIONAL_SELECT","ReplaceScalarD1","ReplaceScalarD2","ReplaceScalarD4","ReplaceScalarD8","ReplaceScalarR4","ReplaceScalarR8","ShuffleD1","BitwiseSelectANY","LoadScalarAndInsertX1","LoadScalarAndInsertX2","LoadScalarAndInsertX4","LoadScalarAndInsertX8","StoreSelectedScalarX1","StoreSelectedScalarX2","StoreSelectedScalarX4","StoreSelectedScalarX8"]},ma={13:[65,0],14:[65,1]},ha={456:168,462:174,457:170,463:176},ga={508:[69,40,54],428:[106,40,54],430:[107,40,54],432:[107,40,54],436:[115,40,54],429:[124,41,55],431:[125,41,55],433:[125,41,55],437:[133,41,55],511:[106,40,54],515:[108,40,54],513:[124,41,55],517:[126,41,55],434:[140,42,56],435:[154,43,57],464:[178,40,56],467:[183,40,57],438:[184,40,57],465:[180,41,56],468:[185,41,57],439:[186,41,57],469:[187,42,57],466:[182,43,56],460:[1,52,55],461:[1,53,55],444:[113,40,54],452:[113,40,54],440:[117,40,54],448:[117,40,54],445:[113,41,54],453:[113,41,54],441:[117,41,54],449:[117,41,54],525:[116,40,54],526:[134,41,55],527:[117,40,54],528:[135,41,55],523:[118,40,54],524:[136,41,55],639:[119,40,54],640:[137,41,55],641:[120,40,54],642:[138,41,55],643:[103,40,54],645:[104,40,54],647:[105,40,54],644:[121,41,55],646:[122,41,55],648:[123,41,55],512:[106,40,54],516:[108,40,54],514:[124,41,55],518:[126,41,55],519:[113,40,54],520:[113,40,54],521:[114,40,54],522:[114,40,54]},ba={394:187,395:1,398:187,399:1,402:187,403:1,406:187,407:1,412:187,413:1,416:187,417:1,426:187,427:1,420:187,421:1,65536:187,65537:187,65535:187,65539:1,65540:1,65538:1},ya={344:[106,40,54],362:[106,40,54],364:[106,40,54],348:[107,40,54],352:[108,40,54],366:[108,40,54],368:[108,40,54],356:[109,40,54],360:[110,40,54],380:[111,40,54],384:[112,40,54],374:[113,40,54],376:[114,40,54],378:[115,40,54],388:[116,40,54],390:[117,40,54],386:[118,40,54],345:[124,41,55],349:[125,41,55],353:[126,41,55],357:[127,41,55],381:[129,41,55],361:[128,41,55],385:[130,41,55],375:[131,41,55],377:[132,41,55],379:[133,41,55],389:[134,41,55],391:[135,41,55],387:[136,41,55],346:[146,42,56],350:[147,42,56],354:[148,42,56],358:[149,42,56],347:[160,43,57],351:[161,43,57],355:[162,43,57],359:[163,43,57],392:[70,40,54],396:[71,40,54],414:[72,40,54],400:[74,40,54],418:[76,40,54],404:[78,40,54],424:[73,40,54],410:[75,40,54],422:[77,40,54],408:[79,40,54],393:[81,41,54],397:[82,41,54],415:[83,41,54],401:[85,41,54],419:[87,41,54],405:[89,41,54],425:[84,41,54],411:[86,41,54],423:[88,41,54],409:[90,41,54]},wa={187:392,207:396,195:400,215:410,199:414,223:424,191:404,211:408,203:418,219:422,231:[392,!1,!0],241:[396,!1,!0],235:[400,!1,!0],245:[410,!1,!0],237:[414,!1,!0],249:[424,!1,!0],233:[404,!1,!0],243:[408,!1,!0],239:[418,!1,!0],247:[422,!1,!0],251:[392,65,!0],261:[396,65,!0],255:[400,65,!0],265:[410,65,!0],257:[414,65,!0],269:[424,65,!0],253:[404,65,!0],263:[408,65,!0],259:[418,65,!0],267:[422,65,!0],188:393,208:397,196:401,216:411,200:415,224:425,192:405,212:409,204:419,220:423,252:[393,66,!0],256:[401,66,!0],266:[411,66,!0],258:[415,66,!0],270:[425,66,!0],254:[405,66,!0],264:[409,66,!0],260:[419,66,!0],268:[423,66,!0],189:394,209:65535,197:402,217:412,201:416,225:426,193:406,213:65536,205:420,221:65537,190:395,210:65538,198:403,218:413,202:417,226:427,194:407,214:65539,206:421,222:65540},ka={599:[!0,!1,159],626:[!0,!0,145],586:[!0,!1,155],613:[!0,!0,141],592:[!0,!1,156],619:[!0,!0,142],603:[!0,!1,153],630:[!0,!0,139],581:[!0,!1,"acos"],608:[!0,!0,"acosf"],582:[!0,!1,"acosh"],609:[!0,!0,"acoshf"],587:[!0,!1,"cos"],614:[!0,!0,"cosf"],579:[!0,!1,"asin"],606:[!0,!0,"asinf"],580:[!0,!1,"asinh"],607:[!0,!0,"asinhf"],598:[!0,!1,"sin"],625:[!0,!0,"sinf"],583:[!0,!1,"atan"],610:[!0,!0,"atanf"],584:[!0,!1,"atanh"],611:[!0,!0,"atanhf"],601:[!0,!1,"tan"],628:[!0,!0,"tanf"],588:[!0,!1,"cbrt"],615:[!0,!0,"cbrtf"],590:[!0,!1,"exp"],617:[!0,!0,"expf"],593:[!0,!1,"log"],620:[!0,!0,"logf"],594:[!0,!1,"log2"],621:[!0,!0,"log2f"],595:[!0,!1,"log10"],622:[!0,!0,"log10f"],604:[!1,!1,164],631:[!1,!0,150],605:[!1,!1,165],632:[!1,!0,151],585:[!1,!1,"atan2"],612:[!1,!0,"atan2f"],596:[!1,!1,"pow"],623:[!1,!0,"powf"],383:[!1,!1,"fmod"],382:[!1,!0,"fmodf"]},Sa={560:[67,0,0],561:[67,192,0],562:[68,0,1],563:[68,193,1],564:[65,0,2],565:[66,0,3]},va={566:[74,0,0],567:[74,192,0],568:[75,0,1],569:[75,193,1],570:[72,0,2],571:[73,0,3]},Ua={652:1,653:2,654:4,655:8},Ea={652:44,653:46,654:40,655:41},Ta={652:58,653:59,654:54,655:55},xa=new Set([20,21,22,23,24,25,26,27,28,29,30]),Ia={51:[16,54],52:[16,54],53:[8,54],54:[8,54],55:[4,54],57:[4,56],56:[2,55],58:[2,57]},Aa={1:[16,40],2:[8,40],3:[4,40],5:[4,42],4:[2,41],6:[2,43]},ja=new Set([81,84,85,86,87,82,83,88,89,90,91,92,93]),$a={13:[16],14:[8],15:[4],16:[2]},La={10:100,11:132,12:164,13:196},Ra={6:[44,23],7:[46,26],8:[40,28],9:[41,30]};function Ba(e,t){return B(e+2*t)}function Na(e,t){return M(e+2*t)}function Ca(e,t){return O(e+2*t)}function Oa(e){return D(e+Ys(4))}function Da(e,t){const n=D(Oa(e)+Ys(5));return D(n+t*fc)}function Fa(e,t){const n=D(Oa(e)+Ys(12));return D(n+t*fc)}function Ma(e,t,n){if(!n)return!1;for(let r=0;r=40||ut(!1,`Expected load opcode but got ${n}`),e.appendU8(n),void 0!==r)e.appendULeb(r);else if(253===n)throw new Error("PREFIX_simd ldloc without a simdOpcode");const o=Ya(t,n,r);e.appendMemarg(t,o)}function ei(e,t,n,r){n>=54||ut(!1,`Expected store opcode but got ${n}`),e.appendU8(n),void 0!==r&&e.appendULeb(r);const o=Ya(t,n,r);e.appendMemarg(t,o),Ja(t),void 0!==r&&Ja(t+8)}function ti(e,t,n){"number"!=typeof n&&(n=512),n>0&&Xa(t,n),e.lea("pLocals",t)}function ni(e,t,n,r){Xa(t,r),Ws(e,t,0,r,!1)||(ti(e,t,r),qs(e,n,r))}function ri(e,t,n,r){if(Xa(t,r),Gs(e,t,n,r,!1))return!0;ti(e,t,r),ti(e,n,0),Js(e,r)}function oi(e,t){return 0!==o.mono_jiterp_is_imethod_var_address_taken(Oa(e.frame),t)}function si(e,t,n,r){if(e.allowNullCheckOptimization&&Ha.has(t)&&!oi(e,t))return la(7,1),void(qa===t?r&&e.local("cknull_ptr"):(Ka(e,t,40),e.local("cknull_ptr",r?34:33),qa=t));Ka(e,t,40),e.local("cknull_ptr",34),e.appendU8(69),e.block(64,4),Ps(e,n,2),e.endBlock(),r&&e.local("cknull_ptr"),e.allowNullCheckOptimization&&!oi(e,t)?(Ha.set(t,n),qa=t):qa=-1}function ai(e,t,n){let r,s=54;const a=ma[n];if(a)e.local("pLocals"),e.appendU8(a[0]),r=a[1],e.appendLeb(r);else switch(n){case 15:e.local("pLocals"),r=Na(t,2),e.i32_const(r);break;case 16:e.local("pLocals"),r=Ca(t,2),e.i32_const(r);break;case 17:e.local("pLocals"),e.i52_const(0),s=55;break;case 19:e.local("pLocals"),e.appendU8(66),e.appendLebRef(t+4,!0),s=55;break;case 18:e.local("pLocals"),e.i52_const(Na(t,2)),s=55;break;case 20:e.local("pLocals"),e.appendU8(67),e.appendF32(function(e,t){return n=e+2*t,o.mono_wasm_get_f32_unaligned(n);var n}(t,2)),s=56;break;case 21:e.local("pLocals"),e.appendU8(68),e.appendF64(function(e,t){return n=e+2*t,o.mono_wasm_get_f64_unaligned(n);var n}(t,2)),s=57;break;default:return!1}e.appendU8(s);const i=Ba(t,1);return e.appendMemarg(i,2),Ja(i),"number"==typeof r?Pa.set(i,{type:"i32",value:r}):Pa.delete(i),!0}function ii(e,t,n){let r=40,o=54;switch(n){case 74:r=44;break;case 75:r=45;break;case 76:r=46;break;case 77:r=47;break;case 78:r=45,o=58;break;case 79:r=47,o=59;break;case 80:break;case 81:r=41,o=55;break;case 82:{const n=Ba(t,3);return ri(e,Ba(t,1),Ba(t,2),n),!0}case 83:return ri(e,Ba(t,1),Ba(t,2),8),ri(e,Ba(t,3),Ba(t,4),8),!0;case 84:return ri(e,Ba(t,1),Ba(t,2),8),ri(e,Ba(t,3),Ba(t,4),8),ri(e,Ba(t,5),Ba(t,6),8),!0;case 85:return ri(e,Ba(t,1),Ba(t,2),8),ri(e,Ba(t,3),Ba(t,4),8),ri(e,Ba(t,5),Ba(t,6),8),ri(e,Ba(t,7),Ba(t,8),8),!0;default:return!1}return e.local("pLocals"),Ka(e,Ba(t,2),r),ei(e,Ba(t,1),o),!0}function ci(e,t,n,r){const o=r>=23&&r<=36||r>=50&&r<=60,s=Ba(n,o?2:1),a=Ba(n,3),i=Ba(n,o?1:2),c=e.allowNullCheckOptimization&&Ha.has(s)&&!oi(e,s);36!==r&&45!==r&&si(e,s,n,!1);let l=54,p=40;switch(r){case 23:p=44;break;case 24:p=45;break;case 25:p=46;break;case 26:p=47;break;case 31:case 41:case 27:break;case 43:case 29:p=42,l=56;break;case 44:case 30:p=43,l=57;break;case 37:case 38:l=58;break;case 39:case 40:l=59;break;case 28:case 42:p=41,l=55;break;case 45:return c||e.block(),e.local("pLocals"),e.i32_const(a),e.i32_const(s),e.i32_const(i),e.callImport("stfld_o"),c?(e.appendU8(26),la(7,1)):(e.appendU8(13),e.appendULeb(0),Ps(e,n,2),e.endBlock()),!0;case 32:{const t=Ba(n,4);return ti(e,i,t),e.local("cknull_ptr"),0!==a&&(e.i32_const(a),e.appendU8(106)),Js(e,t),!0}case 46:{const r=Da(t,Ba(n,4));return e.local("cknull_ptr"),0!==a&&(e.i32_const(a),e.appendU8(106)),ti(e,i,0),e.ptr_const(r),e.callImport("value_copy"),!0}case 47:{const t=Ba(n,4);return e.local("cknull_ptr"),0!==a&&(e.i32_const(a),e.appendU8(106)),ti(e,i,0),Js(e,t),!0}case 36:case 35:return e.local("pLocals"),Ka(e,s,40),0!==a&&(e.i32_const(a),e.appendU8(106)),ei(e,i,l),!0;default:return!1}return o&&e.local("pLocals"),e.local("cknull_ptr"),o?(e.appendU8(p),e.appendMemarg(a,0),ei(e,i,l),!0):(Ka(e,i,p),e.appendU8(l),e.appendMemarg(a,0),!0)}function li(e,t,n,r){const o=r>=23&&r<=36||r>=50&&r<=60,s=Ba(n,1),a=Da(t,Ba(n,2)),i=Da(t,Ba(n,3));!function(e,t,n){e.block(),e.ptr_const(t),e.appendU8(45),e.appendMemarg(Ys(0),0),e.appendU8(13),e.appendULeb(0),Ps(e,n,3),e.endBlock()}(e,a,n);let c=54,l=40;switch(r){case 50:l=44;break;case 51:l=45;break;case 52:l=46;break;case 53:l=47;break;case 58:case 65:case 54:break;case 67:case 56:l=42,c=56;break;case 68:case 57:l=43,c=57;break;case 61:case 62:c=58;break;case 63:case 64:c=59;break;case 55:case 66:l=41,c=55;break;case 69:return e.ptr_const(i),ti(e,s,0),e.callImport("copy_ptr"),!0;case 59:{const t=Ba(n,4);return ti(e,s,t),e.ptr_const(i),Js(e,t),!0}case 72:return e.local("pLocals"),e.ptr_const(i),ei(e,s,c),!0;default:return!1}return o?(e.local("pLocals"),e.ptr_const(i),e.appendU8(l),e.appendMemarg(0,0),ei(e,s,c),!0):(e.ptr_const(i),Ka(e,s,l),e.appendU8(c),e.appendMemarg(0,0),!0)}function pi(e,t,n){let r,o,s,a,i="math_lhs32",c="math_rhs32",l=!1;const p=ba[n];if(p){e.local("pLocals");const r=1==p;return Ka(e,Ba(t,2),r?43:42),r||e.appendU8(p),Ka(e,Ba(t,3),r?43:42),r||e.appendU8(p),e.i32_const(n),e.callImport("relop_fp"),ei(e,Ba(t,1),54),!0}switch(n){case 382:case 383:return hi(e,t,n);default:if(a=ya[n],!a)return!1;a.length>3?(r=a[1],o=a[2],s=a[3]):(r=o=a[1],s=a[2])}switch(n){case 356:case 357:case 360:case 361:case 380:case 381:case 384:case 385:{const s=361===n||385===n||357===n||381===n;i=s?"math_lhs64":"math_lhs32",c=s?"math_rhs64":"math_rhs32",e.block(),Ka(e,Ba(t,2),r),e.local(i,33),Ka(e,Ba(t,3),o),e.local(c,34),l=!0,s&&(e.appendU8(80),e.appendU8(69)),e.appendU8(13),e.appendULeb(0),Ps(e,t,12),e.endBlock(),356!==n&&380!==n&&357!==n&&381!==n||(e.block(),e.local(c),s?e.i52_const(-1):e.i32_const(-1),e.appendU8(s?82:71),e.appendU8(13),e.appendULeb(0),e.local(i),e.appendU8(s?66:65),e.appendBoundaryValue(s?64:32,-1),e.appendU8(s?82:71),e.appendU8(13),e.appendULeb(0),Ps(e,t,13),e.endBlock());break}case 362:case 364:case 366:case 368:Ka(e,Ba(t,2),r),e.local(i,34),Ka(e,Ba(t,3),o),e.local(c,34),e.i32_const(n),e.callImport(364===n||368===n?"ckovr_u4":"ckovr_i4"),e.block(64,4),Ps(e,t,13),e.endBlock(),l=!0}return e.local("pLocals"),l?(e.local(i),e.local(c)):(Ka(e,Ba(t,2),r),Ka(e,Ba(t,3),o)),e.appendU8(a[0]),ei(e,Ba(t,1),s),!0}function ui(e,t,n){const r=ga[n];if(!r)return!1;const o=r[1],s=r[2];switch((n<472||n>507)&&e.local("pLocals"),n){case 428:case 430:Ka(e,Ba(t,2),o),e.i32_const(1);break;case 432:e.i32_const(0),Ka(e,Ba(t,2),o);break;case 436:Ka(e,Ba(t,2),o),e.i32_const(-1);break;case 444:case 445:Ka(e,Ba(t,2),o),41===o&&e.appendU8(167),e.i32_const(255);break;case 452:case 453:Ka(e,Ba(t,2),o),41===o&&e.appendU8(167),e.i32_const(65535);break;case 440:case 441:Ka(e,Ba(t,2),o),41===o&&e.appendU8(167),e.i32_const(24),e.appendU8(116),e.i32_const(24);break;case 448:case 449:Ka(e,Ba(t,2),o),41===o&&e.appendU8(167),e.i32_const(16),e.appendU8(116),e.i32_const(16);break;case 429:case 431:Ka(e,Ba(t,2),o),e.i52_const(1);break;case 433:e.i52_const(0),Ka(e,Ba(t,2),o);break;case 437:Ka(e,Ba(t,2),o),e.i52_const(-1);break;case 511:case 515:case 519:case 521:case 525:case 527:case 523:case 639:case 641:Ka(e,Ba(t,2),o),e.i32_const(Na(t,3));break;case 512:case 516:case 520:case 522:Ka(e,Ba(t,2),o),e.i32_const(Ca(t,3));break;case 513:case 517:case 526:case 528:case 524:case 640:case 642:Ka(e,Ba(t,2),o),e.i52_const(Na(t,3));break;case 514:case 518:Ka(e,Ba(t,2),o),e.i52_const(Ca(t,3));break;default:Ka(e,Ba(t,2),o)}return 1!==r[0]&&e.appendU8(r[0]),ei(e,Ba(t,1),s),!0}function di(e,t,n,r){const o=133===r?t+6:t+8,s=Fa(n,B(o-2));e.local("pLocals"),e.ptr_const(o),e.appendU8(54),e.appendMemarg(s,0),e.callHandlerReturnAddresses.push(o)}function fi(e,t){const n=o.mono_jiterp_get_opcode_info(t,4),r=e+2+2*o.mono_jiterp_get_opcode_info(t,2);let s;switch(n){case 7:s=O(r);break;case 8:s=M(r);break;case 17:s=M(r+2);break;default:return}return s}function _i(e,t,n,r){const s=r>=227&&r<=270,a=fi(t,r);if("number"!=typeof a)return!1;switch(r){case 132:case 133:case 128:case 129:{const s=132===r||133===r,i=t+2*a;return a<=0?e.backBranchOffsets.indexOf(i)>=0?(e.backBranchTraceLevel>1&&Fe(`0x${t.toString(16)} performing backward branch to 0x${i.toString(16)}`),s&&di(e,t,n,r),e.cfg.branch(i,!0,0),la(9,1),!0):(i1||e.cfg.trace>1)&&Fe(`0x${t.toString(16)} ${js(r)} target 0x${i.toString(16)} before start of trace`):(e.backBranchTraceLevel>0||e.cfg.trace>0)&&Fe(`0x${t.toString(16)} ${js(r)} target 0x${i.toString(16)} not found in list `+e.backBranchOffsets.map((e=>"0x"+e.toString(16))).join(", ")),o.mono_jiterp_boost_back_branch_target(i),Ps(e,i,5),la(10,1),!0):(e.branchTargets.add(i),s&&di(e,t,n,r),e.cfg.branch(i,!1,0),!0)}case 145:case 143:case 229:case 227:case 146:case 144:{const n=146===r||144===r;Ka(e,Ba(t,1),n?41:40),143===r||227===r?e.appendU8(69):144===r?e.appendU8(80):146===r&&(e.appendU8(80),e.appendU8(69));break}default:if(void 0===wa[r])throw new Error(`Unsupported relop branch opcode: ${js(r)}`);if(4!==o.mono_jiterp_get_opcode_info(r,1))throw new Error(`Unsupported long branch opcode: ${js(r)}`)}const i=t+2*a;return a<0?e.backBranchOffsets.indexOf(i)>=0?(e.backBranchTraceLevel>1&&Fe(`0x${t.toString(16)} performing conditional backward branch to 0x${i.toString(16)}`),e.cfg.branch(i,!0,s?3:1),la(9,1)):(i1||e.cfg.trace>1)&&Fe(`0x${t.toString(16)} ${js(r)} target 0x${i.toString(16)} before start of trace`):(e.backBranchTraceLevel>0||e.cfg.trace>0)&&Fe(`0x${t.toString(16)} ${js(r)} target 0x${i.toString(16)} not found in list `+e.backBranchOffsets.map((e=>"0x"+e.toString(16))).join(", ")),o.mono_jiterp_boost_back_branch_target(i),e.block(64,4),Ps(e,i,5),e.endBlock(),la(10,1)):(e.branchTargets.add(i),e.cfg.branch(i,!1,s?3:1)),!0}function mi(e,t,n,r){const o=wa[r];if(!o)return!1;const s=Array.isArray(o)?o[0]:o,a=ya[s],i=ba[s];if(!a&&!i)return!1;const c=a?a[1]:1===i?43:42;return Ka(e,Ba(t,1),c),a||1===i||e.appendU8(i),Array.isArray(o)&&o[1]?(e.appendU8(o[1]),e.appendLeb(Na(t,2))):Ka(e,Ba(t,2),c),a||1==i||e.appendU8(i),a?e.appendU8(a[0]):(e.i32_const(s),e.callImport("relop_fp")),_i(e,t,n,r)}function hi(e,t,n){let r,o,s,a;const i=Ba(t,1),c=Ba(t,2),l=Ba(t,3),p=ka[n];if(!p)return!1;if(r=p[0],o=p[1],"string"==typeof p[2]?s=p[2]:a=p[2],e.local("pLocals"),r){if(Ka(e,c,o?42:43),a)e.appendU8(a);else{if(!s)throw new Error("internal error");e.callImport(s)}return ei(e,i,o?56:57),!0}if(Ka(e,c,o?42:43),Ka(e,l,o?42:43),a)e.appendU8(a);else{if(!s)throw new Error("internal error");e.callImport(s)}return ei(e,i,o?56:57),!0}function gi(e,t,n){const r=n>=87&&n<=112,o=n>=107&&n<=112,s=n>=95&&n<=106||n>=120&&n<=127||o,a=n>=101&&n<=106||n>=124&&n<=127||o;let i,c,l=-1,p=0,u=1;o?(i=Ba(t,1),c=Ba(t,2),l=Ba(t,3),p=Na(t,4),u=Na(t,5)):s?a?r?(i=Ba(t,1),c=Ba(t,2),p=Na(t,3)):(i=Ba(t,2),c=Ba(t,1),p=Na(t,3)):r?(i=Ba(t,1),c=Ba(t,2),l=Ba(t,3)):(i=Ba(t,3),c=Ba(t,1),l=Ba(t,2)):r?(c=Ba(t,2),i=Ba(t,1)):(c=Ba(t,1),i=Ba(t,2));let d,f=54;switch(n){case 87:case 95:case 101:case 107:d=44;break;case 88:case 96:case 102:case 108:d=45;break;case 89:case 97:case 103:case 109:d=46;break;case 90:case 98:case 104:case 110:d=47;break;case 113:case 120:case 124:d=40,f=58;break;case 114:case 121:case 125:d=40,f=59;break;case 91:case 99:case 105:case 111:case 115:case 122:case 126:case 119:d=40;break;case 93:case 117:d=42,f=56;break;case 94:case 118:d=43,f=57;break;case 92:case 100:case 106:case 112:case 116:case 123:case 127:d=41,f=55;break;default:return!1}const _=Za(e,c,40,!0,!0);return _||si(e,c,t,!1),r?(e.local("pLocals"),_?ut(Za(e,c,40,!1,!0),"Unknown jiterpreter cprop failure"):e.local("cknull_ptr"),o?(Ka(e,l,40),0!==p&&(e.i32_const(p),e.appendU8(106),p=0),1!==u&&(e.i32_const(u),e.appendU8(108)),e.appendU8(106)):s&&l>=0?(Ka(e,l,40),e.appendU8(106)):p<0&&(e.i32_const(p),e.appendU8(106),p=0),e.appendU8(d),e.appendMemarg(p,0),ei(e,i,f)):119===n?(_?ut(Za(e,c,40,!1,!0),"Unknown jiterpreter cprop failure"):e.local("cknull_ptr"),ti(e,i,0),e.callImport("copy_ptr")):(_?ut(Za(e,c,40,!1,!0),"Unknown jiterpreter cprop failure"):e.local("cknull_ptr"),s&&l>=0?(Ka(e,l,40),e.appendU8(106)):p<0&&(e.i32_const(p),e.appendU8(106),p=0),Ka(e,i,d),e.appendU8(f),e.appendMemarg(p,0)),!0}function bi(e,t,n,r,o){e.block(),Ka(e,r,40),e.local("index",34);let s="cknull_ptr";e.options.zeroPageOptimization&&ra()?(la(8,1),Ka(e,n,40),s="src_ptr",e.local(s,34)):si(e,n,t,!0),e.appendU8(40),e.appendMemarg(Ys(9),2),e.appendU8(73),e.appendU8(13),e.appendULeb(0),Ps(e,t,9),e.endBlock(),e.local(s),e.i32_const(Ys(1)),e.appendU8(106),e.local("index"),1!=o&&(e.i32_const(o),e.appendU8(108)),e.appendU8(106)}function yi(e,t,n,r){const o=r<=328&&r>=315||341===r,s=Ba(n,o?2:1),a=Ba(n,o?1:3),i=Ba(n,o?3:2);let c,l,p=54;switch(r){case 341:return e.local("pLocals"),si(e,s,n,!0),e.appendU8(40),e.appendMemarg(Ys(9),2),ei(e,a,54),!0;case 326:return e.local("pLocals"),l=Ba(n,4),bi(e,n,s,i,l),ei(e,a,54),!0;case 337:return e.block(),Ka(e,Ba(n,1),40),Ka(e,Ba(n,2),40),Ka(e,Ba(n,3),40),e.callImport("stelemr_tc"),e.appendU8(13),e.appendULeb(0),Ps(e,n,10),e.endBlock(),!0;case 340:return bi(e,n,s,i,4),ti(e,a,0),e.callImport("copy_ptr"),!0;case 324:case 320:case 319:case 333:l=4,c=40;break;case 315:l=1,c=44;break;case 316:l=1,c=45;break;case 330:case 329:l=1,c=40,p=58;break;case 317:l=2,c=46;break;case 318:l=2,c=47;break;case 332:case 331:l=2,c=40,p=59;break;case 322:case 335:l=4,c=42,p=56;break;case 321:case 334:l=8,c=41,p=55;break;case 323:case 336:l=8,c=43,p=57;break;case 325:{const t=Ba(n,4);return e.local("pLocals"),e.i32_const(Ba(n,1)),e.appendU8(106),bi(e,n,s,i,t),Js(e,t),Xa(Ba(n,1),t),!0}case 338:{const r=Ba(n,5),o=Da(t,Ba(n,4));return bi(e,n,s,i,r),ti(e,a,0),e.ptr_const(o),e.callImport("value_copy"),!0}case 339:{const t=Ba(n,5);return bi(e,n,s,i,t),ti(e,a,0),Js(e,t),!0}default:return!1}return o?(e.local("pLocals"),bi(e,n,s,i,l),e.appendU8(c),e.appendMemarg(0,0),ei(e,a,p)):(bi(e,n,s,i,l),Ka(e,a,c),e.appendU8(p),e.appendMemarg(0,0)),!0}function wi(){return void 0!==Wa||(Wa=!0===ot.featureWasmSimd,Wa||Fe("Disabling Jiterpreter SIMD")),Wa}function ki(e,t,n){const r=`${t}_${n.toString(16)}`;return"object"!=typeof e.importedFunctions[r]&&e.defineImportedFunction("s",r,t,!1,n),r}function Si(e,t,n,r,s,a){if(e.options.enableSimd&&wi())switch(s){case 2:if(function(e,t,n){const r=o.mono_jiterp_get_simd_opcode(1,n);if(r>=0)return ja.has(n)?(e.local("pLocals"),Ka(e,Ba(t,2),40),e.appendSimd(r,!0),e.appendMemarg(0,0),vi(e,t)):(Ui(e,t),e.appendSimd(r),vi(e,t)),!0;const s=La[n];if(s)return Ui(e,t),e.appendSimd(s),ei(e,Ba(t,1),54),!0;switch(n){case 6:case 7:case 8:case 9:{const r=Ra[n];return e.local("pLocals"),e.v128_const(0),Ka(e,Ba(t,2),r[0]),e.appendSimd(r[1]),e.appendU8(0),ei(e,Ba(t,1),253,11),!0}case 14:return Ui(e,t,7),vi(e,t),!0;case 15:return Ui(e,t,8),vi(e,t),!0;case 16:return Ui(e,t,9),vi(e,t),!0;case 17:return Ui(e,t,10),vi(e,t),!0;default:return!1}}(e,t,a))return!0;break;case 3:if(function(e,t,n){const r=o.mono_jiterp_get_simd_opcode(2,n);if(r>=0){const o=xa.has(n),s=Ia[n];if(o)e.local("pLocals"),Ka(e,Ba(t,2),253,0),Ka(e,Ba(t,3),40),e.appendSimd(r),vi(e,t);else if(Array.isArray(s)){const n=za(e,Ba(t,3)),o=s[0];if("number"!=typeof n)return Pe(`${e.functions[0].name}: Non-constant lane index passed to ExtractScalar`),!1;if(n>=o||n<0)return Pe(`${e.functions[0].name}: ExtractScalar index ${n} out of range (0 - ${o-1})`),!1;e.local("pLocals"),Ka(e,Ba(t,2),253,0),e.appendSimd(r),e.appendU8(n),ei(e,Ba(t,1),s[1])}else Ei(e,t),e.appendSimd(r),vi(e,t);return!0}switch(n){case 191:return Ka(e,Ba(t,2),40),Ka(e,Ba(t,3),253,0),e.appendSimd(11),e.appendMemarg(0,0),!0;case 10:case 11:return Ei(e,t),e.appendSimd(214),e.appendSimd(195),11===n&&e.appendU8(69),ei(e,Ba(t,1),54),!0;case 12:case 13:{const r=13===n,o=r?71:65;return e.local("pLocals"),Ka(e,Ba(t,2),253,0),e.local("math_lhs128",34),Ka(e,Ba(t,3),253,0),e.local("math_rhs128",34),e.appendSimd(o),e.local("math_lhs128"),e.local("math_lhs128"),e.appendSimd(o),e.local("math_rhs128"),e.local("math_rhs128"),e.appendSimd(o),e.appendSimd(80),e.appendSimd(77),e.appendSimd(80),e.appendSimd(r?195:163),ei(e,Ba(t,1),54),!0}case 47:{const n=Ba(t,3),r=za(e,n);return e.local("pLocals"),Ka(e,Ba(t,2),253,0),"object"==typeof r?(e.appendSimd(12),e.appendBytes(r)):Ka(e,n,253,0),e.appendSimd(14),vi(e,t),!0}case 48:case 49:return function(e,t,n){const r=16/n,o=Ba(t,3),s=za(e,o);if(2!==r&&4!==r&&ut(!1,"Unsupported shuffle element size"),e.local("pLocals"),Ka(e,Ba(t,2),253,0),"object"==typeof s){const t=new Uint8Array(_c),o=2===r?new Uint16Array(s.buffer,s.byteOffset,n):new Uint32Array(s.buffer,s.byteOffset,n);for(let e=0,s=0;e=0){const o=Aa[n],s=$a[n];if(Array.isArray(o)){const n=o[0],s=za(e,Ba(t,3));if("number"!=typeof s)return Pe(`${e.functions[0].name}: Non-constant lane index passed to ReplaceScalar`),!1;if(s>=n||s<0)return Pe(`${e.functions[0].name}: ReplaceScalar index ${s} out of range (0 - ${n-1})`),!1;e.local("pLocals"),Ka(e,Ba(t,2),253,0),Ka(e,Ba(t,4),o[1]),e.appendSimd(r),e.appendU8(s),vi(e,t)}else if(Array.isArray(s)){const n=s[0],o=za(e,Ba(t,4));if("number"!=typeof o)return Pe(`${e.functions[0].name}: Non-constant lane index passed to store method`),!1;if(o>=n||o<0)return Pe(`${e.functions[0].name}: Store lane ${o} out of range (0 - ${n-1})`),!1;Ka(e,Ba(t,2),40),Ka(e,Ba(t,3),253,0),e.appendSimd(r),e.appendMemarg(0,0),e.appendU8(o)}else!function(e,t){e.local("pLocals"),Ka(e,Ba(t,2),253,0),Ka(e,Ba(t,3),253,0),Ka(e,Ba(t,4),253,0)}(e,t),e.appendSimd(r),vi(e,t);return!0}switch(n){case 0:return e.local("pLocals"),Ka(e,Ba(t,3),253,0),Ka(e,Ba(t,4),253,0),Ka(e,Ba(t,2),253,0),e.appendSimd(82),vi(e,t),!0;case 7:{const n=za(e,Ba(t,4));if("object"!=typeof n)return Pe(`${e.functions[0].name}: Non-constant indices passed to PackedSimd.Shuffle`),!1;for(let t=0;t<32;t++){const r=n[t];if(r<0||r>31)return Pe(`${e.functions[0].name}: Shuffle lane index #${t} (${r}) out of range (0 - 31)`),!1}return e.local("pLocals"),Ka(e,Ba(t,2),253,0),Ka(e,Ba(t,3),253,0),e.appendSimd(13),e.appendBytes(n),vi(e,t),!0}default:return!1}}(e,t,a))return!0}switch(n){case 651:if(e.options.enableSimd&&wi()){e.local("pLocals");const n=Y().slice(t+4,t+4+_c);e.v128_const(n),vi(e,t),Pa.set(Ba(t,1),{type:"v128",value:n})}else ti(e,Ba(t,1),_c),e.ptr_const(t+4),Js(e,_c);return!0;case 652:case 653:case 654:case 655:{const r=Ua[n],o=_c/r,s=Ba(t,1),a=Ba(t,2),i=Ea[n],c=Ta[n];for(let t=0;t2;return e.local("pLocals"),si(e,Ba(t,2),t,!0),Ka(e,Ba(t,3),n?41:40),e.appendAtomic(r[0],!1),e.appendMemarg(0,r[2]),0!==r[1]&&e.appendU8(r[1]),ei(e,Ba(t,1),n?55:54),!0}const o=va[n];if(o){const n=o[2]>2;return e.local("pLocals"),si(e,Ba(t,2),t,!0),Ka(e,Ba(t,4),n?41:40),Ka(e,Ba(t,3),n?41:40),e.appendAtomic(o[0],!1),e.appendMemarg(0,o[2]),0!==o[1]&&e.appendU8(o[1]),ei(e,Ba(t,1),n?55:54),!0}return!1}const xi=64;let Ii,Ai,ji,$i=0;const Li={};function Ri(){return Ai||(Ai=[ta("interp_entry_prologue",Zs("mono_jiterp_interp_entry_prologue")),ta("interp_entry",Zs("mono_jiterp_interp_entry")),ta("unbox",Zs("mono_jiterp_object_unbox")),ta("stackval_from_data",Zs("mono_jiterp_stackval_from_data"))],Ai)}let Bi,Ni=class{constructor(e,t,n,r,o,s,a,i){this.imethod=e,this.method=t,this.argumentCount=n,this.unbox=o,this.hasThisReference=s,this.hasReturnValue=a,this.paramTypes=new Array(n);for(let e=0;ee&&(n=n.substring(n.length-e,n.length)),n=`${this.imethod.toString(16)}_${n}`}else n=`${this.imethod.toString(16)}_${this.hasThisReference?"i":"s"}${this.hasReturnValue?"_r":""}_${this.argumentCount}`;this.traceName=n}finally{e&&Xe._free(e)}}getTraceName(){return this.traceName||this.generateName(),this.traceName||"unknown"}getName(){return this.name||this.generateName(),this.name||"unknown"}};function Ci(){const e=[];let t=0;for(;0!=(t=o.mono_jiterp_tlqueue_next(1));){const n=Li[t];n?e.push(n):Fe(`Failed to find corresponding info for method ptr ${t} from jit queue!`)}if(!e.length)return;const n=4*e.length+1;let r=Ii;if(r?r.clear(n):(Ii=r=new Ns(n),r.defineType("unbox",{pMonoObject:127},127,!0),r.defineType("interp_entry_prologue",{pData:127,this_arg:127},127,!0),r.defineType("interp_entry",{pData:127,res:127},64,!0),r.defineType("stackval_from_data",{type:127,result:127,value:127},64,!0)),r.options.wasmBytesLimit<=ca(6))return;const s=Ms();let a=0,i=!0,c=!1;try{r.appendU32(1836278016),r.appendU32(1);for(let t=0;tYi[o.mono_jiterp_type_to_ldind(e)])),this.enableDirect=pa().directJitCalls&&!this.noWrapper&&this.wasmNativeReturnType&&(0===this.wasmNativeSignature.length||this.wasmNativeSignature.every((e=>e))),this.enableDirect&&(this.target=this.addr);let c=this.target.toString(16);const l=Hi++;this.name=`${this.enableDirect?"jcp":"jcw"}_${c}_${l.toString(16)}`}}function Xi(e){let t=Wi[e];return t||(e>=Wi.length&&(Wi.length=e+1),Vi||(Vi=zs()),Wi[e]=t=Vi.get(e)),t}function Qi(){const e=[];let t=0;for(;0!=(t=o.mono_jiterp_tlqueue_next(0));){const n=Gi[t];if(n)for(let t=0;t0){o.mono_jiterp_register_jit_call_thunk(n.cinfo,r);for(let e=0;e0&&(gc.push(["trace_eip","trace_eip",Uc]),gc.push(["trace_args","trace_eip",Ec]));const e=(e,t)=>{for(let n=0;n>>0,rc.operand2=t>>>0}function Tc(e,t,n,r){if("number"==typeof r)o.mono_jiterp_adjust_abort_count(r,1),r=js(r);else{let e=uc[r];"number"!=typeof e?e=1:e++,uc[r]=e}dc[e].abortReason=r}function xc(e){if(!ot.runtimeReady)return;if(oc||(oc=pa()),!oc.enableStats)return;const t=ca(9),n=ca(10),r=ca(7),s=ca(8),a=ca(3),i=ca(4),c=ca(2),l=ca(1),p=ca(0),u=ca(6),d=ca(11),f=ca(12),_=t/(t+n)*100,m=o.mono_jiterp_get_rejected_trace_count(),h=oc.eliminateNullChecks?r.toString():"off",g=oc.zeroPageOptimization?s.toString()+(ra()?"":" (disabled)"):"off",b=oc.enableBackwardBranches?`emitted: ${t}, failed: ${n} (${_.toFixed(1)}%)`:": off",y=a?oc.directJitCalls?`direct jit calls: ${i} (${(i/a*100).toFixed(1)}%)`:"direct jit calls: off":"";if(Fe(`// jitted ${u} bytes; ${l} traces (${(l/p*100).toFixed(1)}%) (${m} rejected); ${a} jit_calls; ${c} interp_entries`),Fe(`// cknulls eliminated: ${h}, fused: ${g}; back-branches ${b}; ${y}`),Fe(`// time: ${0|d}ms generating, ${0|f}ms compiling wasm.`),!e){if(oc.countBailouts){const e=Object.values(dc);e.sort(((e,t)=>(t.bailoutCount||0)-(e.bailoutCount||0)));for(let e=0;et.hitCount-e.hitCount)),Fe("// hottest failed traces:");for(let e=0,n=0;e=0)){if(t[e].abortReason){if(t[e].abortReason.startsWith("mono_icall_")||t[e].abortReason.startsWith("ret."))continue;switch(t[e].abortReason){case"trace-too-small":case"trace-too-big":case"call":case"callvirt.fast":case"calli.nat.fast":case"calli.nat":case"call.delegate":case"newobj":case"newobj_vt":case"newobj_slow":case"switch":case"rethrow":case"end-of-body":case"ret":case"intrins_marvin_block":case"intrins_ascii_chars_to_uppercase":continue}}n++,Fe(`${t[e].name} @${t[e].ip} (${t[e].hitCount} hits) ${t[e].abortReason}`)}const n=[];for(const t in e)n.push([t,e[t]]);n.sort(((e,t)=>t[1]-e[1])),Fe("// heat:");for(let e=0;e0?uc[t]=n:delete uc[t]}const e=Object.keys(uc);e.sort(((e,t)=>uc[t]-uc[e]));for(let t=0;te.toString(16).padStart(2,"0"))).join("")}`}async function Rc(e){const t=st.config.resources.lazyAssembly;if(!t)throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly.");let n=e;e.endsWith(".dll")?n=e.substring(0,e.length-4):e.endsWith(".wasm")&&(n=e.substring(0,e.length-5));const r=n+".dll",o=n+".wasm";if(st.config.resources.fingerprinting){const t=st.config.resources.fingerprinting;for(const n in t){const s=t[n];if(s==r||s==o){e=n;break}}}if(!t[e])if(t[r])e=r;else{if(!t[o])throw new Error(`${e} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);e=o}const s={name:e,hash:t[e],behavior:"assembly"};if(st.loadedAssemblies.includes(e))return!1;let a=n+".pdb",i=!1;if(0!=st.config.debugLevel&&(i=Object.prototype.hasOwnProperty.call(t,a),st.config.resources.fingerprinting)){const e=st.config.resources.fingerprinting;for(const t in e)if(e[t]==a){a=t,i=!0;break}}const c=st.retrieve_asset_download(s);let l=null,p=null;if(i){const e=t[a]?st.retrieve_asset_download({name:a,hash:t[a],behavior:"pdb"}):Promise.resolve(null),[n,r]=await Promise.all([c,e]);l=new Uint8Array(n),p=r?new Uint8Array(r):null}else{const e=await c;l=new Uint8Array(e),p=null}return function(e,t){st.assert_runtime_running();const n=Xe.stackSave();try{const n=xn(4),r=In(n,2),o=In(n,3);Mn(r,21),Mn(o,21),yo(r,e,4),yo(o,t,4),gn(mn.LoadLazyAssembly,n)}finally{Xe.stackRestore(n)}}(l,p),!0}async function Bc(e){const t=st.config.resources.satelliteResources;t&&await Promise.all(e.filter((e=>Object.prototype.hasOwnProperty.call(t,e))).map((e=>{const n=[];for(const r in t[e]){const o={name:r,hash:t[e][r],behavior:"resource",culture:e};n.push(st.retrieve_asset_download(o))}return n})).reduce(((e,t)=>e.concat(t)),new Array).map((async e=>{const t=await e;!function(e){st.assert_runtime_running();const t=Xe.stackSave();try{const t=xn(3),n=In(t,2);Mn(n,21),yo(n,e,4),gn(mn.LoadSatelliteAssembly,t)}finally{Xe.stackRestore(t)}}(new Uint8Array(t))})))}function Nc(e){if(e===c)return null;const t=o.mono_wasm_read_as_bool_or_null_unsafe(e);return 0!==t&&(1===t||null)}var Cc,Oc;function Dc(e){if(e)try{(e=e.toLocaleLowerCase()).includes("zh")&&(e=e.replace("chs","HANS").replace("cht","HANT"));const t=Intl.getCanonicalLocales(e.replace("_","-"));return t.length>0?t[0]:void 0}catch(e){return}}!function(e){e[e.Sending=0]="Sending",e[e.Closed=1]="Closed",e[e.Error=2]="Error"}(Cc||(Cc={})),function(e){e[e.Idle=0]="Idle",e[e.PartialCommand=1]="PartialCommand",e[e.Error=2]="Error"}(Oc||(Oc={}));const Fc=[function(e){qo&&(globalThis.clearTimeout(qo),qo=void 0),qo=Xe.safeSetTimeout(mono_wasm_schedule_timer_tick,e)},function(e,t,n,r,o){if(!0!==ot.mono_wasm_runtime_is_ready)return;const s=Y(),a=0!==e?xe(e).concat(".dll"):"",i=dt(new Uint8Array(s.buffer,t,n));let c;r&&(c=dt(new Uint8Array(s.buffer,r,o))),It({eventName:"AssemblyLoaded",assembly_name:a,assembly_b64:i,pdb_b64:c})},function(e,t){const n=xe(t);Qe.logging&&"function"==typeof Qe.logging.debugger&&Qe.logging.debugger(e,n)},function(e,t,n,r){const o={res_ok:e,res:{id:t,value:dt(new Uint8Array(Y().buffer,n,r))}};_t.has(t)&&Me(`Adding an id (${t}) that already exists in commands_received`),_t.set(t,o)},function mono_wasm_fire_debugger_agent_message_with_data(e,t){mono_wasm_fire_debugger_agent_message_with_data_to_pause(dt(new Uint8Array(Y().buffer,e,t)))},mono_wasm_fire_debugger_agent_message_with_data_to_pause,function(){++Jo,Xe.safeSetTimeout(Yo,0)},function(e,t,n,r,s,a,i,c){if(n||ut(!1,"expected instruction pointer"),oc||(oc=pa()),!oc.enableTraces)return 1;if(oc.wasmBytesLimit<=ca(6))return 1;let l,p=dc[r];if(p||(dc[r]=p=new cc(n,r,i)),la(0,1),oc.estimateHeat||ac.length>0||p.isVerbose){const e=o.mono_wasm_method_get_full_name(t);l=xe(e),Xe._free(e)}const u=xe(o.mono_wasm_method_get_name(t));p.name=l||u;let d=oc.noExitBackwardBranches?function(e,t,n){const r=t+n,s=[],a=(e-t)/2;for(;e=a&&s.push(t)}switch(r){case 132:case 133:s.push(n+i)}e+=2*i}else e+=2*i}return s.length<=0?null:new Uint16Array(s)}(n,s,a):null;if(d&&n!==s){const e=(n-s)/2;let t=!1;for(let n=0;n=e){t=!0;break}t||(d=null)}const f=function(e,t,n,r,s,a,i,c,l){let p=hc;p?p.clear(8):(hc=p=new Ns(8),function(e){e.defineType("trace",{frame:127,pLocals:127,cinfo:127,ip:127},127,!0),e.defineType("bailout",{retval:127,base:127,reason:127},127,!0),e.defineType("copy_ptr",{dest:127,src:127},64,!0),e.defineType("value_copy",{dest:127,src:127,klass:127},64,!0),e.defineType("entry",{imethod:127},127,!0),e.defineType("strlen",{ppString:127,pResult:127},127,!0),e.defineType("getchr",{ppString:127,pIndex:127,pResult:127},127,!0),e.defineType("getspan",{destination:127,span:127,index:127,element_size:127},127,!0),e.defineType("overflow_check_i4",{lhs:127,rhs:127,opcode:127},127,!0),e.defineType("mathop_d_d",{value:124},124,!0),e.defineType("mathop_dd_d",{lhs:124,rhs:124},124,!0),e.defineType("mathop_f_f",{value:125},125,!0),e.defineType("mathop_ff_f",{lhs:125,rhs:125},125,!0),e.defineType("fmaf",{x:125,y:125,z:125},125,!0),e.defineType("fma",{x:124,y:124,z:124},124,!0),e.defineType("trace_eip",{traceId:127,eip:127},64,!0),e.defineType("newobj_i",{ppDestination:127,vtable:127},127,!0),e.defineType("newstr",{ppDestination:127,length:127},127,!0),e.defineType("localloc",{destination:127,len:127,frame:127},64,!0),e.defineType("ld_del_ptr",{ppDestination:127,ppSource:127},64,!0),e.defineType("ldtsflda",{ppDestination:127,offset:127},64,!0),e.defineType("gettype",{destination:127,source:127},127,!0),e.defineType("castv2",{destination:127,source:127,klass:127,opcode:127},127,!0),e.defineType("hasparent",{klass:127,parent:127},127,!0),e.defineType("imp_iface",{vtable:127,klass:127},127,!0),e.defineType("imp_iface_s",{obj:127,vtable:127,klass:127},127,!0),e.defineType("box",{vtable:127,destination:127,source:127,vt:127},64,!0),e.defineType("conv",{destination:127,source:127,opcode:127},127,!0),e.defineType("relop_fp",{lhs:124,rhs:124,opcode:127},127,!0),e.defineType("safepoint",{frame:127,ip:127},64,!0),e.defineType("hashcode",{ppObj:127},127,!0),e.defineType("try_hash",{ppObj:127},127,!0),e.defineType("hascsize",{ppObj:127},127,!0),e.defineType("hasflag",{klass:127,dest:127,sp1:127,sp2:127},64,!0),e.defineType("array_rank",{destination:127,source:127},127,!0),e.defineType("stfld_o",{locals:127,fieldOffsetBytes:127,targetLocalOffsetBytes:127,sourceLocalOffsetBytes:127},127,!0),e.defineType("notnull",{ptr:127,expected:127,traceIp:127,ip:127},64,!0),e.defineType("stelemr",{o:127,aindex:127,ref:127},127,!0),e.defineType("simd_p_p",{arg0:127,arg1:127},64,!0),e.defineType("simd_p_pp",{arg0:127,arg1:127,arg2:127},64,!0),e.defineType("simd_p_ppp",{arg0:127,arg1:127,arg2:127,arg3:127},64,!0);const t=vc();for(let n=0;ni.indexOf(e)>=0))>=0;b&&!i&&ut(!1,"Expected methodFullName if trace is instrumented");const y=b?pc++:0;b&&(Fe(`instrumenting: ${i}`),lc[y]=new ic(i)),p.compressImportNames=!b;try{p.appendU32(1836278016),p.appendU32(1),p.generateTypeSection();const t={disp:127,cknull_ptr:127,dest_ptr:127,src_ptr:127,memop_dest:127,memop_src:127,index:127,count:127,math_lhs32:127,math_rhs32:127,math_lhs64:126,math_rhs64:126,temp_f32:125,temp_f64:124};p.options.enableSimd&&(t.v128_zero=123,t.math_lhs128=123,t.math_rhs128=123);let s=!0,i=0;if(p.defineFunction({type:"trace",name:d,export:!0,locals:t},(()=>{switch(p.base=n,p.traceIndex=a,p.frame=e,B(n)){case 673:case 674:case 676:case 675:break;default:throw new Error(`Expected *ip to be a jiterpreter opcode but it was ${B(n)}`)}return p.cfg.initialize(r,c,b?1:0),i=function(e,t,n,r,s,a,i,c){let l=!0,p=!1,u=!1,d=!1,f=0,_=0,m=0;Ga(),a.backBranchTraceLevel=i?2:0;let h=a.cfg.entry(n);for(;n&&n;){if(a.cfg.ip=n,n>=s){Tc(a.traceIndex,0,0,"end-of-body"),i&&Fe(`instrumented trace ${t} exited at end of body @${n.toString(16)}`);break}const g=3840-a.bytesGeneratedSoFar-a.cfg.overheadBytes;if(a.size>=g){Tc(a.traceIndex,0,0,"trace-too-big"),i&&Fe(`instrumented trace ${t} exited because of size limit at @${n.toString(16)} (spaceLeft=${g}b)`);break}let b=B(n);const y=o.mono_jiterp_get_opcode_info(b,2),w=o.mono_jiterp_get_opcode_info(b,3),k=o.mono_jiterp_get_opcode_info(b,1),S=b>=656&&b<=658,v=S?b-656+2:0,U=S?Ba(n,1+v):0;b>=0&&b<690||ut(!1,`invalid opcode ${b}`);const E=S?_a[v][U]:js(b),T=n,x=a.options.noExitBackwardBranches&&Ma(n,r,c),I=a.branchTargets.has(n),A=x||I||l&&c,j=m+_+a.branchTargets.size;let $=!1,L=ea(b);switch(x&&(a.backBranchTraceLevel>1&&Fe(`${t} recording back branch target 0x${n.toString(16)}`),a.backBranchOffsets.push(n)),A&&(u=!1,d=!1,Qa(a,n,x),p=!0,Ga(),m=0),L<-1&&p&&(L=-2===L?2:0),l=!1,271===b||(sc.indexOf(b)>=0?(Ps(a,n,23),b=677):u&&(b=677)),b){case 677:u&&(d||a.appendU8(0),d=!0);break;case 313:case 314:ni(a,Ba(n,1),0,Ba(n,2));break;case 312:ti(a,Ba(n,1)),Ka(a,Ba(n,2),40),a.local("frame"),a.callImport("localloc");break;case 285:Ka(a,Ba(n,1),40),a.i32_const(0),Ka(a,Ba(n,2),40),a.appendU8(252),a.appendU8(11),a.appendU8(0);break;case 286:Ka(a,Ba(n,1),40),qs(a,0,Ba(n,2));break;case 310:{const e=Ba(n,3),t=Ba(n,2),r=Ba(n,1),o=za(a,e);0!==o&&("number"!=typeof o?(Ka(a,e,40),a.local("count",34),a.block(64,4)):(a.i32_const(o),a.local("count",33)),Ka(a,r,40),a.local("dest_ptr",34),a.appendU8(69),Ka(a,t,40),a.local("src_ptr",34),a.appendU8(69),a.appendU8(114),a.block(64,4),Ps(a,n,2),a.endBlock(),"number"==typeof o&&Gs(a,0,0,o,!1,"dest_ptr","src_ptr")||(a.local("dest_ptr"),a.local("src_ptr"),a.local("count"),a.appendU8(252),a.appendU8(10),a.appendU8(0),a.appendU8(0)),"number"!=typeof o&&a.endBlock());break}case 311:{const e=Ba(n,3),t=Ba(n,2);si(a,Ba(n,1),n,!0),Ka(a,t,40),Ka(a,e,40),a.appendU8(252),a.appendU8(11),a.appendU8(0);break}case 143:case 145:case 227:case 229:case 144:case 146:case 129:case 132:case 133:_i(a,n,e,b)?p=!0:n=0;break;case 538:{const e=Ba(n,2),t=Ba(n,1);e!==t?(a.local("pLocals"),si(a,e,n,!0),ei(a,t,54)):si(a,e,n,!1),a.allowNullCheckOptimization&&Ha.set(t,n),$=!0;break}case 637:case 638:{const t=D(e+Ys(4));a.ptr_const(t),a.callImport("entry"),a.block(64,4),Ps(a,n,1),a.endBlock();break}case 675:L=0;break;case 138:break;case 86:{a.local("pLocals");const e=Ba(n,2),r=oi(a,e),o=Ba(n,1);r||Pe(`${t}: Expected local ${e} to have address taken flag`),ti(a,e),ei(a,o,54),Pa.set(o,{type:"ldloca",offset:e}),$=!0;break}case 272:case 300:case 301:case 556:{a.local("pLocals");let t=Da(e,Ba(n,2));300===b&&(t=o.mono_jiterp_imethod_to_ftnptr(t)),a.ptr_const(t),ei(a,Ba(n,1),54);break}case 305:{const t=Da(e,Ba(n,3));Ka(a,Ba(n,1),40),Ka(a,Ba(n,2),40),a.ptr_const(t),a.callImport("value_copy");break}case 306:{const e=Ba(n,3);Ka(a,Ba(n,1),40),Ka(a,Ba(n,2),40),Js(a,e);break}case 307:{const e=Ba(n,3);ti(a,Ba(n,1),e),si(a,Ba(n,2),n,!0),Js(a,e);break}case 308:{const t=Da(e,Ba(n,3));Ka(a,Ba(n,1),40),ti(a,Ba(n,2),0),a.ptr_const(t),a.callImport("value_copy");break}case 309:{const e=Ba(n,3);Ka(a,Ba(n,1),40),ti(a,Ba(n,2),0),Js(a,e);break}case 540:a.local("pLocals"),si(a,Ba(n,2),n,!0),a.appendU8(40),a.appendMemarg(Ys(2),2),ei(a,Ba(n,1),54);break;case 539:{a.block(),Ka(a,Ba(n,3),40),a.local("index",34);let e="cknull_ptr";a.options.zeroPageOptimization&&ra()?(la(8,1),Ka(a,Ba(n,2),40),e="src_ptr",a.local(e,34)):si(a,Ba(n,2),n,!0),a.appendU8(40),a.appendMemarg(Ys(2),2),a.appendU8(72),a.local("index"),a.i32_const(0),a.appendU8(78),a.appendU8(113),a.appendU8(13),a.appendULeb(0),Ps(a,n,11),a.endBlock(),a.local("pLocals"),a.local("index"),a.i32_const(2),a.appendU8(108),a.local(e),a.appendU8(106),a.appendU8(47),a.appendMemarg(Ys(3),1),ei(a,Ba(n,1),54);break}case 342:case 343:{const e=Na(n,4);a.block(),Ka(a,Ba(n,3),40),a.local("index",34);let t="cknull_ptr";342===b?si(a,Ba(n,2),n,!0):(ti(a,Ba(n,2),0),t="src_ptr",a.local(t,34)),a.appendU8(40),a.appendMemarg(Ys(7),2),a.appendU8(73),a.local("index"),a.i32_const(0),a.appendU8(78),a.appendU8(113),a.appendU8(13),a.appendULeb(0),Ps(a,n,18),a.endBlock(),a.local("pLocals"),a.local(t),a.appendU8(40),a.appendMemarg(Ys(8),2),a.local("index"),a.i32_const(e),a.appendU8(108),a.appendU8(106),ei(a,Ba(n,1),54);break}case 663:a.block(),Ka(a,Ba(n,3),40),a.local("count",34),a.i32_const(0),a.appendU8(78),a.appendU8(13),a.appendULeb(0),Ps(a,n,18),a.endBlock(),ti(a,Ba(n,1),16),a.local("dest_ptr",34),Ka(a,Ba(n,2),40),a.appendU8(54),a.appendMemarg(0,0),a.local("dest_ptr"),a.local("count"),a.appendU8(54),a.appendMemarg(4,0);break;case 577:ti(a,Ba(n,1),8),ti(a,Ba(n,2),8),a.callImport("ld_del_ptr");break;case 73:ti(a,Ba(n,1),4),a.ptr_const(Ca(n,2)),a.callImport("ldtsflda");break;case 662:a.block(),ti(a,Ba(n,1),4),ti(a,Ba(n,2),0),a.callImport("gettype"),a.appendU8(13),a.appendULeb(0),Ps(a,n,2),a.endBlock();break;case 659:{const t=Da(e,Ba(n,4));a.ptr_const(t),ti(a,Ba(n,1),4),ti(a,Ba(n,2),0),ti(a,Ba(n,3),0),a.callImport("hasflag");break}case 668:{const e=Ys(1);a.local("pLocals"),si(a,Ba(n,2),n,!0),a.i32_const(e),a.appendU8(106),ei(a,Ba(n,1),54);break}case 660:a.local("pLocals"),ti(a,Ba(n,2),0),a.callImport("hashcode"),ei(a,Ba(n,1),54);break;case 661:a.local("pLocals"),ti(a,Ba(n,2),0),a.callImport("try_hash"),ei(a,Ba(n,1),54);break;case 664:a.local("pLocals"),ti(a,Ba(n,2),0),a.callImport("hascsize"),ei(a,Ba(n,1),54);break;case 669:a.local("pLocals"),Ka(a,Ba(n,2),40),a.local("math_lhs32",34),Ka(a,Ba(n,3),40),a.appendU8(115),a.i32_const(2),a.appendU8(116),a.local("math_rhs32",33),a.local("math_lhs32"),a.i32_const(327685),a.appendU8(106),a.i32_const(10485920),a.appendU8(114),a.i32_const(1703962),a.appendU8(106),a.i32_const(-8388737),a.appendU8(114),a.local("math_rhs32"),a.appendU8(113),a.appendU8(69),ei(a,Ba(n,1),54);break;case 541:case 542:a.block(),ti(a,Ba(n,1),4),ti(a,Ba(n,2),0),a.callImport(541===b?"array_rank":"a_elesize"),a.appendU8(13),a.appendULeb(0),Ps(a,n,2),a.endBlock();break;case 289:case 290:{const t=Da(e,Ba(n,3)),r=o.mono_jiterp_is_special_interface(t),s=289===b,i=Ba(n,1);if(!t){Tc(a.traceIndex,0,0,"null-klass"),n=0;continue}a.block(),a.options.zeroPageOptimization&&ra()?(Ka(a,Ba(n,2),40),a.local("dest_ptr",34),la(8,1)):(a.block(),Ka(a,Ba(n,2),40),a.local("dest_ptr",34),a.appendU8(13),a.appendULeb(0),a.local("pLocals"),a.i32_const(0),ei(a,i,54),a.appendU8(12),a.appendULeb(1),a.endBlock(),a.local("dest_ptr")),r&&a.local("dest_ptr"),a.appendU8(40),a.appendMemarg(Ys(14),0),a.ptr_const(t),a.callImport(r?"imp_iface_s":"imp_iface"),s&&(a.local("dest_ptr"),a.appendU8(69),a.appendU8(114)),a.block(64,4),a.local("pLocals"),a.local("dest_ptr"),ei(a,i,54),a.appendU8(5),s?Ps(a,n,19):(a.local("pLocals"),a.i32_const(0),ei(a,i,54)),a.endBlock(),a.endBlock();break}case 291:case 292:case 287:case 288:{const t=Da(e,Ba(n,3)),r=291===b||292===b,o=287===b||291===b,s=Ba(n,1);if(!t){Tc(a.traceIndex,0,0,"null-klass"),n=0;continue}a.block(),a.options.zeroPageOptimization&&ra()?(Ka(a,Ba(n,2),40),a.local("dest_ptr",34),la(8,1)):(a.block(),Ka(a,Ba(n,2),40),a.local("dest_ptr",34),a.appendU8(13),a.appendULeb(0),a.local("pLocals"),a.i32_const(0),ei(a,s,54),a.appendU8(12),a.appendULeb(1),a.endBlock(),a.local("dest_ptr")),a.appendU8(40),a.appendMemarg(Ys(14),0),a.appendU8(40),a.appendMemarg(Ys(15),0),r&&a.local("src_ptr",34),a.i32_const(t),a.appendU8(70),a.block(64,4),a.local("pLocals"),a.local("dest_ptr"),ei(a,s,54),a.appendU8(5),r?(a.local("src_ptr"),a.ptr_const(t),a.callImport("hasparent"),o&&(a.local("dest_ptr"),a.appendU8(69),a.appendU8(114)),a.block(64,4),a.local("pLocals"),a.local("dest_ptr"),ei(a,s,54),a.appendU8(5),o?Ps(a,n,19):(a.local("pLocals"),a.i32_const(0),ei(a,s,54)),a.endBlock()):(ti(a,Ba(n,1),4),a.local("dest_ptr"),a.ptr_const(t),a.i32_const(b),a.callImport("castv2"),a.appendU8(69),a.block(64,4),Ps(a,n,19),a.endBlock()),a.endBlock(),a.endBlock();break}case 295:case 296:a.ptr_const(Da(e,Ba(n,3))),ti(a,Ba(n,1),4),ti(a,Ba(n,2),0),a.i32_const(296===b?1:0),a.callImport("box");break;case 299:{const t=Da(e,Ba(n,3)),r=Ys(17),o=Ba(n,1),s=D(t+r);if(!t||!s){Tc(a.traceIndex,0,0,"null-klass"),n=0;continue}a.options.zeroPageOptimization&&ra()?(Ka(a,Ba(n,2),40),a.local("dest_ptr",34),la(8,1)):(si(a,Ba(n,2),n,!0),a.local("dest_ptr",34)),a.appendU8(40),a.appendMemarg(Ys(14),0),a.appendU8(40),a.appendMemarg(Ys(15),0),a.local("src_ptr",34),a.appendU8(40),a.appendMemarg(r,0),a.i32_const(s),a.appendU8(70),a.local("src_ptr"),a.appendU8(45),a.appendMemarg(Ys(16),0),a.appendU8(69),a.appendU8(113),a.block(64,4),a.local("pLocals"),a.local("dest_ptr"),a.i32_const(Ys(18)),a.appendU8(106),ei(a,o,54),a.appendU8(5),Ps(a,n,21),a.endBlock();break}case 294:a.block(),ti(a,Ba(n,1),4),Ka(a,Ba(n,2),40),a.callImport("newstr"),a.appendU8(13),a.appendULeb(0),Ps(a,n,17),a.endBlock();break;case 283:a.block(),ti(a,Ba(n,1),4),a.ptr_const(Da(e,Ba(n,2))),a.callImport("newobj_i"),a.appendU8(13),a.appendULeb(0),Ps(a,n,17),a.endBlock();break;case 282:case 284:case 544:case 543:p?(Vs(a,n,j,15),u=!0,L=0):n=0;break;case 546:case 547:case 548:case 549:case 545:p?(Vs(a,n,j,545==b?22:15),u=!0):n=0;break;case 137:case 134:Ps(a,n,16),u=!0;break;case 130:case 131:Ps(a,n,26),u=!0;break;case 136:if(a.callHandlerReturnAddresses.length>0&&a.callHandlerReturnAddresses.length<=3){const t=Fa(e,Ba(n,1));a.local("pLocals"),a.appendU8(40),a.appendMemarg(t,0),a.local("index",33);for(let e=0;e=3&&b<=12||b>=509&&b<=510?p||a.options.countBailouts?(Ps(a,n,14),u=!0):n=0:b>=13&&b<=21?ai(a,n,b)?$=!0:n=0:b>=74&&b<=85?ii(a,n,b)||(n=0):b>=344&&b<=427?pi(a,n,b)||(n=0):ga[b]?ui(a,n,b)||(n=0):wa[b]?mi(a,n,e,b)?p=!0:n=0:b>=23&&b<=49?ci(a,e,n,b)||(n=0):b>=50&&b<=73?li(a,e,n,b)||(n=0):b>=87&&b<=127?gi(a,n,b)||(n=0):b>=579&&b<=632?hi(a,n,b)||(n=0):b>=315&&b<=341?yi(a,e,n,b)||(n=0):b>=227&&b<=270?a.branchTargets.size>0?(Vs(a,n,j,8),u=!0):n=0:b>=651&&b<=658?(a.containsSimd=!0,Si(a,n,b,E,v,U)?$=!0:n=0):b>=559&&b<=571?(a.containsAtomics=!0,Ti(a,n,b)||(n=0)):0===L||(n=0)}if(n){if(!$){const e=n+2;for(let t=0;t0&&(e+=" -> ");for(let n=0;n0&&(p?m++:_++,f+=L),(n+=2*k)<=s&&(h=n)}else i&&Fe(`instrumented trace ${t} aborted for opcode ${E} @${T.toString(16)}`),Tc(a.traceIndex,0,0,b)}for(;a.activeBlocks>0;)a.endBlock();return a.cfg.exitIp=h,a.containsSimd&&(f+=10240),f}(e,d,n,r,u,p,y,c),s=i>=oc.minimumTraceValue,p.cfg.generate()})),p.emitImportsAndFunctions(!1),!s)return g&&"end-of-body"===g.abortReason&&(g.abortReason="trace-too-small"),0;_=Ms();const f=p.getArrayView();if(la(6,f.length),f.length>=4080)return Me(`Jiterpreter generated too much code (${f.length} bytes) for trace ${d}. Please report this issue.`),0;const h=new WebAssembly.Module(f),w=p.getWasmImports(),k=new WebAssembly.Instance(h,w).exports[d];let S;m=!1,l?(zs().set(l,k),S=l):S=Hs(0,k);const v=ca(1);return p.options.enableStats&&v&&v%500==0&&xc(!0),S}catch(e){h=!0,m=!1;let t=p.containsSimd?" (simd)":"";return p.containsAtomics&&(t+=" (atomics)"),Pe(`${i||d}${t} code generation failed: ${e} ${e.stack}`),Xs(),0}finally{const e=Ms();if(_?(la(11,_-f),la(12,e-_)):la(11,e-f),h||!m&&oc.dumpTraces||b){if(h||oc.dumpTraces||b)for(let e=0;e0;)p.endBlock();p.inSection&&p.endSection()}catch(e){}const n=p.getArrayView();for(let r=0;r=4?Ci():$i>0||"function"==typeof globalThis.setTimeout&&($i=globalThis.setTimeout((()=>{$i=0,Ci()}),10))}},function(e,t,n,r,o,s,a,i){if(n>16)return 0;const c=new Ni(e,t,n,r,o,s,a,i);ji||(ji=zs());const l=ji.get(i),p=(s?a?29:20:a?11:2)+n;return c.result=Hs(p,l),Li[e]=c,c.result},function(e,t,n,r,s){const a=D(n+0),i=qi[a];if(i)return void(i.result>0?o.mono_jiterp_register_jit_call_thunk(n,i.result):(i.queue.push(n),i.queue.length>12&&Qi()));const c=new Ji(e,t,n,r,0!==s);qi[a]=c;const l=o.mono_jiterp_tlqueue_add(0,e);let p=Gi[e];p||(p=Gi[e]=[]),p.push(c),l>=6&&Qi()},function(e,t,n,r,s){const a=Xi(e);try{a(t,n,r,s)}catch(e){const t=Xe.wasmExports.__cpp_exception,n=t instanceof WebAssembly.Tag;if(n&&!(e instanceof WebAssembly.Exception&&e.is(t)))throw e;if(i=s,Xe.HEAPU32[i>>>2]=1,n){const n=e.getArg(t,0);o.mono_jiterp_begin_catch(n),o.mono_jiterp_end_catch()}else{if("number"!=typeof e)throw e;o.mono_jiterp_begin_catch(e),o.mono_jiterp_end_catch()}}var i},Qi,function(e,t,n){delete dc[n],function(e){delete Li[e]}(t),function(e){const t=Gi[e];if(t){for(let e=0;e{e&&e.dispose()},u=!0)}const d=jn(e,1),f=$n(d),_=Qr(d,f,1),m=26==f,h=20==f||30==f,g={fn:i,fqn:s+":"+o,args_count:c,arg_marshalers:l,res_converter:_,has_cleanup:u,arg_cleanup:p,is_discard_no_wait:m,is_async:h,isDisposed:!1};let b;b=h||m||u?nr(g):0!=c||_?1!=c||_?1==c&&_?function(e){const t=e.fn,r=e.arg_marshalers[0],o=e.res_converter,s=e.fqn;return e=null,function(a){const i=Bt();try{n&&e.isDisposed;const s=r(a),i=t(s);o(a,i)}catch(e){ho(a,e)}finally{Nt(i,"mono.callCsFunction:",s)}}}(g):2==c&&_?function(e){const t=e.fn,r=e.arg_marshalers[0],o=e.arg_marshalers[1],s=e.res_converter,a=e.fqn;return e=null,function(i){const c=Bt();try{n&&e.isDisposed;const a=r(i),c=o(i),l=t(a,c);s(i,l)}catch(e){ho(i,e)}finally{Nt(c,"mono.callCsFunction:",a)}}}(g):nr(g):function(e){const t=e.fn,r=e.arg_marshalers[0],o=e.fqn;return e=null,function(s){const a=Bt();try{n&&e.isDisposed;const o=r(s);t(o)}catch(e){ho(s,e)}finally{Nt(a,"mono.callCsFunction:",o)}}}(g):function(e){const t=e.fn,r=e.fqn;return e=null,function(o){const s=Bt();try{n&&e.isDisposed,t()}catch(e){ho(o,e)}finally{Nt(s,"mono.callCsFunction:",r)}}}(g);let y=b;y[vn]=g,tr[a]=y,Nt(t,"mono.bindJsFunction:",o)}(e),0}catch(e){return $e(function(e){let t="unknown exception";if(e){t=e.toString();const n=e.stack;n&&(n.startsWith(t)?t=n:t+="\n"+n),t=We(t)}return t}(e))}},function(e,t){!function(e,t){st.assert_runtime_running();const n=Nr(e);n&&"function"==typeof n&&n[Sn]||ut(!1,`Bound function handle expected ${e}`),n(t)}(e,t)},function(e,t){st.assert_runtime_running();const n=tr[e];n||ut(!1,`Imported function handle expected ${e}`),n(t)},function(e){fr((()=>function(e){if(!st.is_runtime_running())return void(st.diagnosticTracing&&De("This promise resolution/rejection can't be propagated to managed code, mono runtime already exited."));const t=In(e,0),r=n;try{st.assert_runtime_running();const n=In(e,1),o=In(e,2),s=In(e,3),a=Dn(o),i=qn(o),c=Nr(i);c||ut(!1,`Cannot find Promise for JSHandle ${i}`),c.resolve_or_reject(a,i,s),r||(Mn(n,1),Mn(t,0))}catch(e){ho(t,e)}}(e)))},function(e){fr((()=>function(e){if(!st.is_runtime_running())return void(st.diagnosticTracing&&De("This promise can't be canceled, mono runtime already exited."));const t=Vr(e);t||ut(!1,`Expected Promise for GCHandle ${e}`),t.cancel()}(e)))},function(e,t,n,r,o,s,a){return"function"==typeof at.mono_wasm_change_case?at.mono_wasm_change_case(e,t,n,r,o,s,a):0},function(e,t,n,r,o,s,a,i){return"function"==typeof at.mono_wasm_compare_string?at.mono_wasm_compare_string(e,t,n,r,o,s,a,i):0},function(e,t,n,r,o,s,a,i){return"function"==typeof at.mono_wasm_starts_with?at.mono_wasm_starts_with(e,t,n,r,o,s,a,i):0},function(e,t,n,r,o,s,a,i){return"function"==typeof at.mono_wasm_ends_with?at.mono_wasm_ends_with(e,t,n,r,o,s,a,i):0},function(e,t,n,r,o,s,a,i,c){return"function"==typeof at.mono_wasm_index_of?at.mono_wasm_index_of(e,t,n,r,o,s,a,i,c):0},function(e,t,n,r,o,s){return"function"==typeof at.mono_wasm_get_calendar_info?at.mono_wasm_get_calendar_info(e,t,n,r,o,s):0},function(e,t,n,r,o){return"function"==typeof at.mono_wasm_get_culture_info?at.mono_wasm_get_culture_info(e,t,n,r,o):0},function(e,t,n){return"function"==typeof at.mono_wasm_get_first_day_of_week?at.mono_wasm_get_first_day_of_week(e,t,n):0},function(e,t,n){return"function"==typeof at.mono_wasm_get_first_week_of_year?at.mono_wasm_get_first_week_of_year(e,t,n):0},function(e,t,n,r,o,s,a){try{const i=Ie(n,n+2*r),c=Dc(i);if(!c&&i)return je(o,o+2*i.length,i),v(a,i.length),0;const l=Dc(Ie(e,e+2*t));if(!c||!l)throw new Error(`Locale or culture name is null or empty. localeName=${c}, cultureName=${l}`);const p=c.split("-");let u,d;try{const e=p.length>1?p.pop():void 0;d=e?new Intl.DisplayNames([l],{type:"region"}).of(e):void 0;const t=p.join("-");u=new Intl.DisplayNames([l],{type:"language"}).of(t)}catch(e){if(!(e instanceof RangeError))throw e;try{u=new Intl.DisplayNames([l],{type:"language"}).of(c)}catch(e){if(e instanceof RangeError&&i)return je(o,o+2*i.length,i),v(a,i.length),0;throw e}}const f={LanguageName:u,RegionName:d},_=Object.values(f).join("##");if(!_)throw new Error(`Locale info for locale=${c} is null or empty.`);if(_.length>s)throw new Error(`Locale info for locale=${c} exceeds length of ${s}.`);return je(o,o+2*_.length,_),v(a,_.length),0}catch(e){return v(a,-1),$e(e.toString())}}];async function Mc(e,t){try{const n=await Pc(e,t);return st.mono_exit(n),n}catch(e){try{st.mono_exit(1,e)}catch(e){}return e&&"number"==typeof e.status?e.status:1}}async function Pc(e,t){null!=e&&""!==e||(e=st.config.mainAssemblyName)||ut(!1,"Null or empty config.mainAssemblyName"),null==t&&(t=ot.config.applicationArguments),null==t&&(t=Ye?(await import(/*! webpackIgnore: true */"process")).argv.slice(2):[]),function(e,t){const n=t.length+1,r=Xe._malloc(4*n);let s=0;Xe.setValue(r+4*s,o.mono_wasm_strdup(e),"i32"),s+=1;for(let e=0;e{const t=setInterval((()=>{1==ot.waitForDebugger&&(clearInterval(t),e())}),100)})));try{return Xe.runtimeKeepalivePush(),await new Promise((e=>globalThis.setTimeout(e,0))),await function(e,t,n){st.assert_runtime_running();const r=Xe.stackSave();try{const r=xn(5),o=In(r,1),s=In(r,2),a=In(r,3),i=In(r,4),c=function(e){const t=Xe.lengthBytesUTF8(e)+1,n=Xe._malloc(t),r=Y().subarray(n,n+t);return Xe.stringToUTF8Array(e,r,0,t),r[t-1]=0,n}(e);io(s,c),wo(a,t&&!t.length?void 0:t,15),Zr(i,n);let l=tn(o,0,Ht);return hn(ot.managedThreadTID,mn.CallEntrypoint,r),l=nn(r,Ht,l),null==l&&(l=Promise.resolve(0)),l[Br]=!0,l}finally{Xe.stackRestore(r)}}(e,t,1==ot.waitForDebugger)}finally{Xe.runtimeKeepalivePop()}}function Vc(e){ot.runtimeReady&&(ot.runtimeReady=!1,o.mono_wasm_exit(e))}function zc(e){if(st.exitReason=e,ot.runtimeReady){ot.runtimeReady=!1;const t=qe(e);Xe.abort(t)}throw e}async function Hc(e){e.out||(e.out=console.log.bind(console)),e.err||(e.err=console.error.bind(console)),e.print||(e.print=e.out),e.printErr||(e.printErr=e.err),st.out=e.print,st.err=e.printErr,await async function(){var e;if(Ye){if(globalThis.performance===Uo){const{performance:e}=Qe.require("perf_hooks");globalThis.performance=e}if(Qe.process=await import(/*! webpackIgnore: true */"process"),globalThis.crypto||(globalThis.crypto={}),!globalThis.crypto.getRandomValues){let e;try{e=Qe.require("node:crypto")}catch(e){}e?e.webcrypto?globalThis.crypto=e.webcrypto:e.randomBytes&&(globalThis.crypto.getRandomValues=t=>{t&&t.set(e.randomBytes(t.length))}):globalThis.crypto.getRandomValues=()=>{throw new Error("Using node without crypto support. To enable current operation, either provide polyfill for 'globalThis.crypto.getRandomValues' or enable 'node:crypto' module.")}}}ot.subtle=null===(e=globalThis.crypto)||void 0===e?void 0:e.subtle}()}function Wc(e){const t=Bt();e.locateFile||(e.locateFile=e.__locateFile=e=>st.scriptDirectory+e),e.mainScriptUrlOrBlob=st.scriptUrl;const a=e.instantiateWasm,c=e.preInit?"function"==typeof e.preInit?[e.preInit]:e.preInit:[],l=e.preRun?"function"==typeof e.preRun?[e.preRun]:e.preRun:[],p=e.postRun?"function"==typeof e.postRun?[e.postRun]:e.postRun:[],u=e.onRuntimeInitialized?e.onRuntimeInitialized:()=>{};e.instantiateWasm=(e,t)=>function(e,t,n){const r=Bt();if(n){const o=n(e,((e,n)=>{Nt(r,"mono.instantiateWasm"),ot.afterInstantiateWasm.promise_control.resolve(),t(e,n)}));return o}return async function(e,t){try{await st.afterConfigLoaded,st.diagnosticTracing&&De("instantiate_wasm_module"),await ot.beforePreInit.promise,Xe.addRunDependency("instantiate_wasm_module"),await async function(){ot.featureWasmSimd=await st.simd(),ot.featureWasmEh=await st.exceptions(),ot.emscriptenBuildOptions.wasmEnableSIMD&&(ot.featureWasmSimd||ut(!1,"This browser/engine doesn't support WASM SIMD. Please use a modern version. See also https://aka.ms/dotnet-wasm-features")),ot.emscriptenBuildOptions.wasmEnableEH&&(ot.featureWasmEh||ut(!1,"This browser/engine doesn't support WASM exception handling. Please use a modern version. See also https://aka.ms/dotnet-wasm-features"))}(),function(e){const t=e.env||e.a;if(!t)return void Me("WARNING: Neither imports.env or imports.a were present when instantiating the wasm module. This likely indicates an emscripten configuration issue.");const n=new Array(Fc.length);for(const e in t){const r=t[e];if("function"==typeof r&&-1!==r.toString().indexOf("runtime_idx"))try{const{runtime_idx:t}=r();if(void 0!==n[t])throw new Error(`Duplicate runtime_idx ${t}`);n[t]=e}catch(e){}}for(const[e,r]of Fc.entries()){const o=n[e];if(void 0!==o){if("function"!=typeof t[o])throw new Error(`Expected ${o} to be a function`);t[o]=r}}}(e);const n=await st.wasmCompilePromise.promise;t(await WebAssembly.instantiate(n,e),n),st.diagnosticTracing&&De("instantiate_wasm_module done"),ot.afterInstantiateWasm.promise_control.resolve()}catch(e){throw Pe("instantiate_wasm_module() failed",e),st.mono_exit(1,e),e}Xe.removeRunDependency("instantiate_wasm_module")}(e,t),[]}(e,t,a),e.preInit=[()=>function(e){Xe.addRunDependency("mono_pre_init");const t=Bt();try{Xe.addRunDependency("mono_wasm_pre_init_essential"),st.diagnosticTracing&&De("mono_wasm_pre_init_essential"),st.gitHash!==ot.gitHash&&Me(`The version of dotnet.runtime.js ${ot.gitHash} is different from the version of dotnet.js ${st.gitHash}!`),st.gitHash!==ot.emscriptenBuildOptions.gitHash&&Me(`The version of dotnet.native.js ${ot.emscriptenBuildOptions.gitHash} is different from the version of dotnet.js ${st.gitHash}!`),n!==ot.emscriptenBuildOptions.wasmEnableThreads&&Me(`The threads of dotnet.native.js ${ot.emscriptenBuildOptions.wasmEnableThreads} is different from the version of dotnet.runtime.js ${n}!`),function(){const e=[...r];for(const t of e){const e=o,[n,r,s,a,c]=t,l="function"==typeof n;if(!0===n||l)e[r]=function(...t){!l||!n()||ut(!1,`cwrap ${r} should not be called when binding was skipped`);const o=i(r,s,a,c);return e[r]=o,o(...t)};else{const t=i(r,s,a,c);e[r]=t}}}(),a=Qe,Object.assign(a,{mono_wasm_exit:o.mono_wasm_exit,mono_wasm_profiler_init_aot:s.mono_wasm_profiler_init_aot,mono_wasm_profiler_init_browser:s.mono_wasm_profiler_init_browser,mono_wasm_exec_regression:o.mono_wasm_exec_regression,mono_wasm_print_thread_dump:void 0}),Xe.removeRunDependency("mono_wasm_pre_init_essential"),st.diagnosticTracing&&De("preInit"),ot.beforePreInit.promise_control.resolve(),e.forEach((e=>e()))}catch(e){throw Pe("user preInint() failed",e),st.mono_exit(1,e),e}var a;(async()=>{try{await async function(){st.diagnosticTracing&&De("mono_wasm_pre_init_essential_async"),Xe.addRunDependency("mono_wasm_pre_init_essential_async"),Xe.removeRunDependency("mono_wasm_pre_init_essential_async")}(),Nt(t,"mono.preInit")}catch(e){throw st.mono_exit(1,e),e}ot.afterPreInit.promise_control.resolve(),Xe.removeRunDependency("mono_pre_init")})()}(c)],e.preRun=[()=>async function(e){Xe.addRunDependency("mono_pre_run_async");try{await ot.afterInstantiateWasm.promise,await ot.afterPreInit.promise,st.diagnosticTracing&&De("preRunAsync");const t=Bt();e.map((e=>e())),Nt(t,"mono.preRun")}catch(e){throw Pe("preRunAsync() failed",e),st.mono_exit(1,e),e}ot.afterPreRun.promise_control.resolve(),Xe.removeRunDependency("mono_pre_run_async")}(l)],e.onRuntimeInitialized=()=>async function(e){try{await ot.afterPreRun.promise,st.diagnosticTracing&&De("onRuntimeInitialized"),ot.nativeExit=Vc,ot.nativeAbort=zc;const t=Bt();if(ot.beforeOnRuntimeInitialized.promise_control.resolve(),await ot.coreAssetsInMemory.promise,ot.config.virtualWorkingDirectory){const e=Xe.FS,t=ot.config.virtualWorkingDirectory;try{const n=e.stat(t);n?n&&e.isDir(n.mode)||ut(!1,`FS.chdir: ${t} is not a directory`):Xe.FS_createPath("/",t,!0,!0)}catch(e){Xe.FS_createPath("/",t,!0,!0)}e.chdir(t)}ot.config.interpreterPgo&&setTimeout(Gc,1e3*(ot.config.interpreterPgoSaveDelay||15)),Xe.runtimeKeepalivePush(),n||await async function(){try{const t=Bt();st.diagnosticTracing&&De("Initializing mono runtime");for(const e in ot.config.environmentVariables){const t=ot.config.environmentVariables[e];if("string"!=typeof t)throw new Error(`Expected environment variable '${e}' to be a string but it was ${typeof t}: '${t}'`);qc(e,t)}ot.config.runtimeOptions&&function(e){if(!Array.isArray(e))throw new Error("Expected runtimeOptions to be an array of strings");const t=Xe._malloc(4*e.length);let n=0;for(let r=0;raot; in your project file."),null==e&&(e={}),"writeAt"in e||(e.writeAt="System.Runtime.InteropServices.JavaScript.JavaScriptExports::StopProfile"),"sendTo"in e||(e.sendTo="Interop/Runtime::DumpAotProfileData");const t="aot:write-at-method="+e.writeAt+",send-to-method="+e.sendTo;s.mono_wasm_profiler_init_aot(t)}(ot.config.aotProfilerOptions),ot.config.browserProfilerOptions&&(ot.config.browserProfilerOptions,ot.emscriptenBuildOptions.enableBrowserProfiler||ut(!1,"Browser profiler is not enabled, please use browser; in your project file."),s.mono_wasm_profiler_init_browser("browser:")),ot.config.logProfilerOptions&&(e=ot.config.logProfilerOptions,ot.emscriptenBuildOptions.enableLogProfiler||ut(!1,"Log profiler is not enabled, please use log; in your project file."),e.takeHeapshot||ut(!1,"Log profiler is not enabled, the takeHeapshot method must be defined in LogProfilerOptions.takeHeapshot"),s.mono_wasm_profiler_init_log((e.configuration||"log:alloc,output=output.mlpd")+`,take-heapshot-method=${e.takeHeapshot}`)),function(){st.diagnosticTracing&&De("mono_wasm_load_runtime");try{const e=Bt();let t=ot.config.debugLevel;null==t&&(t=0,ot.config.debugLevel&&(t=0+t)),o.mono_wasm_load_runtime(t),Nt(e,"mono.loadRuntime")}catch(e){throw Pe("mono_wasm_load_runtime () failed",e),st.mono_exit(1,e),e}}(),function(){if(da)return;da=!0;const e=pa(),t=e.tableSize,n=ot.emscriptenBuildOptions.runAOTCompilation?e.tableSize:1,r=ot.emscriptenBuildOptions.runAOTCompilation?e.aotTableSize:1,s=t+n+36*r+1,a=zs();let i=a.length;const c=performance.now();a.grow(s);const l=performance.now();e.enableStats&&Fe(`Allocated ${s} function table entries for jiterpreter, bringing total table size to ${a.length}`),i=ua(0,i,t,Zs("mono_jiterp_placeholder_trace")),i=ua(1,i,n,Zs("mono_jiterp_placeholder_jit_call"));for(let e=2;e<=37;e++)i=ua(e,i,r,a.get(o.mono_jiterp_get_interp_entry_func(e)));const p=performance.now();e.enableStats&&Fe(`Growing wasm function table took ${l-c}. Filling table took ${p-l}.`)}(),function(){if(!ot.mono_wasm_bindings_is_ready){st.diagnosticTracing&&De("bindings_init"),ot.mono_wasm_bindings_is_ready=!0;try{const e=Bt();he||("undefined"!=typeof TextDecoder&&(be=new TextDecoder("utf-16le"),ye=new TextDecoder("utf-8",{fatal:!1}),we=new TextDecoder("utf-8"),ke=new TextEncoder),he=Xe._malloc(12)),Se||(Se=function(e){let t;if(le.length>0)t=le.pop();else{const e=function(){if(null==ae||!ie){ae=ue(se,"js roots"),ie=new Int32Array(se),ce=se;for(let e=0;est.loadedFiles.push(e.url))),st.diagnosticTracing&&De("all assets are loaded in wasm memory"))}(),Xc.registerRuntime(rt),0===st.config.debugLevel||ot.mono_wasm_runtime_is_ready||function mono_wasm_runtime_ready(){if(Qe.mono_wasm_runtime_is_ready=ot.mono_wasm_runtime_is_ready=!0,yt=0,bt={},wt=-1,globalThis.dotnetDebugger)debugger}(),0!==st.config.debugLevel&&st.config.cacheBootResources&&st.logDownloadStatsToConsole(),setTimeout((()=>{st.purgeUnusedCacheEntriesAsync()}),st.config.cachedResourcesPurgeDelay);try{e()}catch(e){throw Pe("user callback onRuntimeInitialized() failed",e),e}await async function(){st.diagnosticTracing&&De("mono_wasm_after_user_runtime_initialized");try{if(Xe.onDotnetReady)try{await Xe.onDotnetReady()}catch(e){throw Pe("onDotnetReady () failed",e),e}}catch(e){throw Pe("mono_wasm_after_user_runtime_initialized () failed",e),e}}(),Nt(t,"mono.onRuntimeInitialized")}catch(e){throw Xe.runtimeKeepalivePop(),Pe("onRuntimeInitializedAsync() failed",e),st.mono_exit(1,e),e}ot.afterOnRuntimeInitialized.promise_control.resolve()}(u),e.postRun=[()=>async function(e){try{await ot.afterOnRuntimeInitialized.promise,st.diagnosticTracing&&De("postRunAsync");const t=Bt();Xe.FS_createPath("/","usr",!0,!0),Xe.FS_createPath("/","usr/share",!0,!0),e.map((e=>e())),Nt(t,"mono.postRun")}catch(e){throw Pe("postRunAsync() failed",e),st.mono_exit(1,e),e}ot.afterPostRun.promise_control.resolve()}(p)],e.ready.then((async()=>{await ot.afterPostRun.promise,Nt(t,"mono.emscriptenStartup"),ot.dotnetReady.promise_control.resolve(rt)})).catch((e=>{ot.dotnetReady.promise_control.reject(e)})),e.ready=ot.dotnetReady.promise}function qc(e,t){o.mono_wasm_setenv(e,t)}async function Gc(){void 0!==st.exitCode&&0!==st.exitCode||await Ac()}async function Jc(e){}let Xc;function Qc(r){const o=Xe,s=r,a=globalThis;Object.assign(s.internal,{mono_wasm_exit:e=>{Xe.err("early exit "+e)},forceDisposeProxies:Hr,mono_wasm_dump_threads:void 0,logging:void 0,mono_wasm_stringify_as_error_with_stack:qe,mono_wasm_get_loaded_files:Is,mono_wasm_send_dbg_command_with_parms:St,mono_wasm_send_dbg_command:vt,mono_wasm_get_dbg_command_info:Ut,mono_wasm_get_details:$t,mono_wasm_release_object:Rt,mono_wasm_call_function_on:jt,mono_wasm_debugger_resume:Et,mono_wasm_detach_debugger:Tt,mono_wasm_raise_debug_event:It,mono_wasm_change_debugger_log_level:xt,mono_wasm_debugger_attached:At,mono_wasm_runtime_is_ready:ot.mono_wasm_runtime_is_ready,mono_wasm_get_func_id_to_name_mappings:Je,get_property:sr,set_property:or,has_property:ar,get_typeof_property:ir,get_global_this:cr,get_dotnet_instance:()=>rt,dynamic_import:ur,mono_wasm_bind_cs_function:hr,ws_wasm_create:hs,ws_wasm_open:gs,ws_wasm_send:bs,ws_wasm_receive:ys,ws_wasm_close:ws,ws_wasm_abort:ks,ws_get_state:ms,http_wasm_supports_streaming_request:Ao,http_wasm_supports_streaming_response:jo,http_wasm_create_controller:$o,http_wasm_get_response_type:Fo,http_wasm_get_response_status:Mo,http_wasm_abort:Ro,http_wasm_transform_stream_write:Bo,http_wasm_transform_stream_close:No,http_wasm_fetch:Do,http_wasm_fetch_stream:Co,http_wasm_fetch_bytes:Oo,http_wasm_get_response_header_names:Po,http_wasm_get_response_header_values:Vo,http_wasm_get_response_bytes:Ho,http_wasm_get_response_length:zo,http_wasm_get_streamed_response_bytes:Wo,jiterpreter_dump_stats:xc,jiterpreter_apply_options:ia,jiterpreter_get_options:pa,interp_pgo_load_data:jc,interp_pgo_save_data:Ac,mono_wasm_gc_lock:re,mono_wasm_gc_unlock:oe,monoObjectAsBoolOrNullUnsafe:Nc,monoStringToStringUnsafe:Ce,loadLazyAssembly:Rc,loadSatelliteAssemblies:Bc});const i={stringify_as_error_with_stack:qe,instantiate_symbols_asset:Ts,instantiate_asset:Es,jiterpreter_dump_stats:xc,forceDisposeProxies:Hr,instantiate_segmentation_rules_asset:xs};"hybrid"===st.config.globalizationMode&&(i.stringToUTF16=je,i.stringToUTF16Ptr=$e,i.utf16ToString=Ie,i.utf16ToStringLoop=Ae,i.localHeapViewU16=Z,i.setU16_local=y,i.setI32=v),Object.assign(ot,i);const c={runMain:Pc,runMainAndExit:Mc,exit:st.mono_exit,setEnvironmentVariable:qc,getAssemblyExports:yr,setModuleImports:rr,getConfig:()=>ot.config,invokeLibraryInitializers:st.invokeLibraryInitializers,setHeapB32:m,setHeapB8:h,setHeapU8:g,setHeapU16:b,setHeapU32:w,setHeapI8:k,setHeapI16:S,setHeapI32:v,setHeapI52:E,setHeapU52:T,setHeapI64Big:x,setHeapF32:I,setHeapF64:A,getHeapB32:$,getHeapB8:L,getHeapU8:R,getHeapU16:B,getHeapU32:N,getHeapI8:F,getHeapI16:M,getHeapI32:P,getHeapI52:V,getHeapU52:z,getHeapI64Big:H,getHeapF32:W,getHeapF64:q,localHeapViewU8:Y,localHeapViewU16:Z,localHeapViewU32:K,localHeapViewI8:G,localHeapViewI16:J,localHeapViewI32:X,localHeapViewI64Big:Q,localHeapViewF32:ee,localHeapViewF64:te};return Object.assign(rt,{INTERNAL:s.internal,Module:o,runtimeBuildInfo:{productVersion:e,gitHash:ot.gitHash,buildConfiguration:t,wasmEnableThreads:n,wasmEnableSIMD:!0,wasmEnableExceptionHandling:!0},...c}),a.getDotnetRuntime?Xc=a.getDotnetRuntime.__list:(a.getDotnetRuntime=e=>a.getDotnetRuntime.__list.getRuntime(e),a.getDotnetRuntime.__list=Xc=new Yc),rt}class Yc{constructor(){this.list={}}registerRuntime(e){return void 0===e.runtimeId&&(e.runtimeId=Object.keys(this.list).length),this.list[e.runtimeId]=mr(e),st.config.runtimeId=e.runtimeId,e.runtimeId}getRuntime(e){const t=this.list[e];return t?t.deref():void 0}}export{Wc as configureEmscriptenStartup,Hc as configureRuntimeStartup,Jc as configureWorkerStartup,Qc as initializeExports,Eo as initializeReplacements,ct as passEmscriptenInternals,Xc as runtimeList,lt as setRuntimeGlobals};
//# sourceMappingURL=dotnet.runtime.js.map
================================================
FILE: docfx/AppBundle/_framework/supportFiles/0_JetBrainsMono-LICENSE.txt
================================================
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
================================================
FILE: docfx/AppBundle/_framework/supportFiles/2_lighting.fs
================================================
#version 100
precision mediump float;
// Input vertex attributes (from vertex shader)
varying vec3 fragPosition;
varying vec2 fragTexCoord;
varying vec4 fragColor;
varying vec3 fragNormal;
// Input uniform values
uniform sampler2D texture0;
uniform vec4 colDiffuse;
// NOTE: Add here your custom variables
#define MAX_LIGHTS 4
#define LIGHT_DIRECTIONAL 0
#define LIGHT_POINT 1
struct MaterialProperty {
vec3 color;
int useSampler;
sampler2D sampler;
};
struct Light {
int enabled;
int type;
vec3 position;
vec3 target;
vec4 color;
};
// Input lighting values
uniform Light lights[MAX_LIGHTS];
uniform vec4 ambient;
uniform vec3 viewPos;
void main()
{
// Texel color fetching from texture sampler
vec4 texelColor = texture2D(texture0, fragTexCoord);
vec3 lightDot = vec3(0.0);
vec3 normal = normalize(fragNormal);
vec3 viewD = normalize(viewPos - fragPosition);
vec3 specular = vec3(0.0);
// NOTE: Implement here your fragment shader code
for (int i = 0; i < MAX_LIGHTS; i++)
{
if (lights[i].enabled == 1)
{
vec3 light = vec3(0.0);
if (lights[i].type == LIGHT_DIRECTIONAL) light = -normalize(lights[i].target - lights[i].position);
if (lights[i].type == LIGHT_POINT) light = normalize(lights[i].position - fragPosition);
float NdotL = max(dot(normal, light), 0.0);
lightDot += lights[i].color.rgb*NdotL;
float specCo = 0.0;
if (NdotL > 0.0) specCo = pow(max(0.0, dot(viewD, reflect(-(light), normal))), 16.0); // Shine: 16.0
specular += specCo;
}
}
vec4 finalColor = (texelColor*((colDiffuse + vec4(specular,1))*vec4(lightDot, 1.0)));
finalColor += texelColor*(ambient/10.0);
// Gamma correction
gl_FragColor = pow(finalColor, vec4(1.0/2.2));
}
================================================
FILE: docfx/AppBundle/_framework/supportFiles/3_lighting.vs
================================================
#version 100
// Input vertex attributes
attribute vec3 vertexPosition;
attribute vec2 vertexTexCoord;
attribute vec3 vertexNormal;
attribute vec4 vertexColor;
// Input uniform values
uniform mat4 mvp;
uniform mat4 matModel;
// Output vertex attributes (to fragment shader)
varying vec3 fragPosition;
varying vec2 fragTexCoord;
varying vec4 fragColor;
varying vec3 fragNormal;
// NOTE: Add here your custom variables
// https://github.com/glslify/glsl-inverse
mat3 inverse(mat3 m)
{
float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2];
float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2];
float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2];
float b01 = a22*a11 - a12*a21;
float b11 = -a22*a10 + a12*a20;
float b21 = a21*a10 - a11*a20;
float det = a00*b01 + a01*b11 + a02*b21;
return mat3(b01, (-a22*a01 + a02*a21), (a12*a01 - a02*a11),
b11, (a22*a00 - a02*a20), (-a12*a00 + a02*a10),
b21, (-a21*a00 + a01*a20), (a11*a00 - a01*a10))/det;
}
// https://github.com/glslify/glsl-transpose
mat3 transpose(mat3 m)
{
return mat3(m[0][0], m[1][0], m[2][0],
m[0][1], m[1][1], m[2][1],
m[0][2], m[1][2], m[2][2]);
}
void main()
{
// Send vertex attributes to fragment shader
fragPosition = vec3(matModel*vec4(vertexPosition, 1.0));
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
mat3 normalMatrix = transpose(inverse(mat3(matModel)));
fragNormal = normalize(normalMatrix*vertexNormal);
// Calculate final vertex position
gl_Position = mvp*vec4(vertexPosition, 1.0);
}
================================================
FILE: docfx/AppBundle/index.html
================================================
Jitter2 WebDemo
⭐ View on GitHub
Use ← → to switch scenes and press R to reset.
================================================
FILE: docfx/AppBundle/main.js
================================================
import { dotnet } from './_framework/dotnet.js';
async function initialize() {
const canvas = document.getElementById('canvas');
const params = new URLSearchParams(window.location.search);
const lightTheme = params.get('theme') === 'light';
// Force an opaque WebGL backbuffer so browser page colors cannot bleed
// through anti-aliased canvas content such as UI text.
const originalGetContext = canvas.getContext.bind(canvas);
canvas.getContext = function(type, attrs) {
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
return originalGetContext(type, {
...attrs,
alpha: false
});
}
return originalGetContext(type, attrs);
};
const { getAssemblyExports, getConfig, runMain } = await dotnet
.withDiagnosticTracing(false)
.create();
const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
dotnet.instance.Module['canvas'] = canvas;
function pointerToCanvasPixels(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const localX = clientX - rect.left;
const localY = clientY - rect.top;
const scaleX = rect.width > 0 ? canvas.width / rect.width : 1;
const scaleY = rect.height > 0 ? canvas.height / rect.height : 1;
return {
x: localX * scaleX,
y: localY * scaleY
};
}
function resizeCanvasToDisplaySize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const cssWidth = canvas.clientWidth;
const cssHeight = canvas.clientHeight;
const pixelWidth = Math.max(1, Math.round(cssWidth * dpr));
const pixelHeight = Math.max(1, Math.round(cssHeight * dpr));
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth;
canvas.height = pixelHeight;
}
exports.WebDemo.Application.Resize(pixelWidth, pixelHeight);
}
function mainLoop() {
resizeCanvasToDisplaySize();
exports.WebDemo.Application.UpdateFrame();
window.requestAnimationFrame(mainLoop);
}
function queuePointerTap(clientX, clientY) {
const point = pointerToCanvasPixels(clientX, clientY);
exports.WebDemo.Application.PointerTap(point.x, point.y);
}
window.addEventListener('resize', resizeCanvasToDisplaySize);
window.addEventListener('orientationchange', resizeCanvasToDisplaySize);
canvas.addEventListener('pointerdown', evt => {
queuePointerTap(evt.clientX, evt.clientY);
if (evt.pointerType === 'touch') {
evt.preventDefault();
}
}, { passive: false });
await runMain();
exports.WebDemo.Application.SetTheme(lightTheme);
resizeCanvasToDisplaySize();
document.getElementById('spinner')?.remove();
window.requestAnimationFrame(mainLoop);
}
initialize().catch(err => {
console.error('An error occurred during initialization:', err);
});
================================================
FILE: docfx/AppBundle/package.json
================================================
{ "type":"module" }
================================================
FILE: docfx/docfx.json
================================================
{
"$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json",
"metadata": [
{
"src": [
{
"src": "../src/Jitter2",
"files": [
"**/*.csproj"
]
}
],
"dest": "api"
}
],
"build": {
"content": [
{
"files": [
"**/*.{md,yml}"
],
"exclude": [
"_site/**"
]
}
],
"resource": [
{
"files": [
"images/**",
"docs/**/images/**",
"logo.png",
"favicon.ico",
"AppBundle/**"
]
}
],
"output": "_site",
"template": [
"default",
"modern",
"template"
],
"globalMetadata": {
"_appName": "",
"_appTitle": "Jitter2",
"_appFaviconPath": "favicon.ico",
"_appLogoPath": "logo.png",
"_enableSearch": true,
"pdf": false,
"_gitUrlPattern": "github",
"_gitContribute": {
"repo": "https://github.com/notgiven688/jitterphysics2",
"branch": "main"
}
}
}
}
================================================
FILE: docfx/docs/changelog.md
================================================
# Changelog
### Jitter 2.8.4 (2026-04-19)
- Improved `CollisionManifold` generation by performing full clipping, producing better contact sets.
- Fixed indexed `TriangleMesh` adjacency generation for meshes with duplicated seam vertices.
- Added generic methods in `ShapeHelper`.
- Improved XML documentation throughout the codebase.
### Jitter 2.8.3 (2026-04-13)
- Added `SolveMode.Deterministic` - an optional cross-platform deterministic solver mode.
- Fixed bugs in sleeping logic.
- Added `PersistentContactManifold` property.
### Jitter 2.8.2 (2026-03-30)
- Added `DynamicTree.SweepCast` and `DynamicTree.FindNearest` - new tree queries combining broadphase pruning with exact narrowphase tests. `SweepCast` sweeps a shape through the tree; `FindNearest` finds the closest proxy by distance. Both include bounded variants and convenience overloads for common shapes.
- Added `World.Stabilize` - solves existing contacts and constraints at the velocity level without advancing body transforms. Useful for warm-starting a restored scene before resuming normal simulation.
### Jitter 2.8.1 (2026-03-21)
- Fixed velocity setter bug in `RigidBody` - introduced in 2.8.0.
- Minor API improvements.
### Jitter 2.8.0 (2026-03-19)
- Added `RigidBody.AddImpulse` methods for applying instantaneous impulses (linear and at a world-space position).
- Fixed corner case in wake-up logic: setting velocity to zero no longer unnecessarily activates sleeping bodies.
- Introduced `ISink` interface and `CollectionSink` adapter as a struct-friendly alternative to `ICollection` on hot paths.
- Added `ref TSink` overloads for `ShapeHelper.Tessellate` and `DynamicTree.Query`.
- Extracted default constraint tuning values into named constants on the `Constraint` base class (`DefaultLinearBias`, `DefaultLinearSoftness`, `DefaultAngularBias`, etc.).
- **Breaking Change:** `DynamicTree.Nodes` changed from `Node[]` to `ReadOnlySpan`, exposing only the occupied portion of the internal array.
- **Breaking Change:** `World.DebugTimings` is now `ReadOnlySpan` instead of `double[]`.
- Renamed generic type parameters in `NarrowPhase` and `CollisionManifold` from `TA`/`TB` to `Ta`/`Tb`.
### Jitter 2.7.9 (2026-02-14)
- Fixed distance calculation in `SpringConstraint`.
- Fixed register bug in `UniversalJoint`.
- Fixed `ConeLimit.Limit` property and added upper bound validation.
- Fixed inconsistent friction clamping between scalar and SIMD contact solver paths. Both paths now use the pre-update normal impulse for the friction cone.
- Fixed unnecessary wake-ups for sleeping bodies in contact with static bodies.
- Added basic debug draw methods to constraints.
- Added world ownership validation in `World.Remove` and `World.CreateConstraint`. Passing bodies, constraints, or arbiters from a different world now throws `ArgumentException`.
- Improved XML documentation throughout the codebase.
### Jitter 2.7.8 (2026-01-18)
- Fixed degraded tree quality for large proxies in `DynamicTree`.
- Added `JQuaternion.Lerp`, `JQuaternion.Slerp`, `JQuaternion.Dot`, and `JQuaternion.Inverse` methods for quaternion interpolation and math.
- Added `JTriangle.GetCenter`, `JTriangle.GetNormal`, `JTriangle.GetArea`, `JTriangle.GetBoundingBox`, and `JTriangle.ClosestPoint` helper methods.
- Added `JBoundingBox.Disjoint` and `JBoundingBox.Contains` methods (also in `TreeBox`).
- Added `World.GetArbiter(id0, id1)` to retrieve existing arbiters without creating new ones.
- Improved XML documentation throughout the codebase.
- Fixed `CalculateMassInertia` in `TransformedShape` returning incorrect values.
- Fixed normalization bug in `TriangleEdgeCollisionFilter`.
- Fixed validation in `SphereShape` radius setter.
- Fixed a bug in speculative contact implementation.
- Improved `TriangleMesh`. Added option to build directly from vertices and indices.
- Added overloads for `Span` throughout the engine (in `TriangleMesh`, `ShapeHelper`, `PointCloudShape`, `ConvexHullShape`).
### Jitter 2.7.7 (2026-01-08)
- Added `vector.UnsafeAs` and `JVector.UnsafeFrom`. Same for `JQuaternion`.
- Added `world.PreSubStep` and `world.PostSubStep` events.
### Jitter 2.7.6 (2025-12-18)
- Removed `World.Capacity`. Number of entities in `World` no longer has to be specified in advance.
- Fixed a bug in `TwistAngle`.
- Added `WeldJoint`.
- Exposed more properties in `ConeLimit`.
### Jitter 2.7.5 (2025-11-16)
- Improved cost heuristic for `DynamicTree`.
### Jitter 2.7.4 (2025-10-29)
- Add `MotionType` property to `RigidBody` instances. Bodies might me `Dynamic`, `Static` or `Kinematic`.
Static bodies with non-zero velocity are no longer supported, kinematic bodies must be used instead.
- The contact graph information is used to optimize the memory layout for contacts (`ReorderContacts` step). This makes the solver more cache friendly.
- Reduced overhead of `CheckDeactivation` step in the engine.
### Jitter 2.7.3 (2025-10-19)
- Improved multi-threading performance.
### Jitter 2.7.2 (2025-09-21)
- Reduced GC in `DynamicTree.Optimize`.
- Default to LocalRayCast if body is not set for `RigidBodyShape`.
- Added wakeup parameter to `AddForce` overloads for finer activation state control.
- Added generic `ICloneableShape` interface for type-safe shape cloning.
### Jitter 2.7.1 (2025-06-28)
- Added `RigidBody.EnableGyroscopicForces` to include gyroscopic forces.
### Jitter 2.7.0 (2025-06-14)
- **Dropped .NET7 support**
- Added `JQuaternion.ToAxisAngle` method
- Renamed `JBBox` to `JBoundingBox`and `TreeBBox` to `TreeBox`.
- Various smaller API changes.
### Jitter 2.6.7 (2025-06-09)
- Introduced SIMD accelerated `TreeBBox` for `DynamicTree`.
### Jitter 2.6.6 (2025-05-31)
- Implicit conversion for `JVector` and `JQuaternion` from tuples, e.g. `cube.Position = (1, 2, 3);`.
- Added `PreStep` and `PostStep` to `World.Timings`.
- Added `NarrowPhase.Sweep` overload which calculates time of impact (TOI) for rotating shapes.
- `RegisterContact` no longer requires a `penetration` and a `speculative` parameter.
- Bugfix: `MathHelper.RotationQuaternion`, fixed wrong/not normalized quaternion generation for large `dt`.
- Added an additional `normal` out-parameter to `NarrowPhase.Distance`.
- Renamed `JVector.TransposedTransform(in JVector vector, in JQuaternion quat)` to `ConjugatedTransform`.
- Added `Anchor1` and `Anchor2` properties to `BallSocket`.
- Bugfix: Skipping degenerate triangles in `TriangleMesh` now works correctly.
### Jitter 2.6.5 (2025-05-21)
- Rigid bodies now activate on velocity or force changes.
- Removed FatTriangleShape.
- Renamed 'Active' to 'ActiveCount' and add span-based accessors in ReadOnlyPartitionedSet.
- Fixed bug in TriangleEdgeCollisionFilter.
### Jitter 2.6.4 (2025-05-19)
- **Breaking Change:** Triangle winding order in `TriangleMesh` is now counter-clockwise (CCW) for front-facing triangles.
*If you're using `TriangleMesh`, swap the vertex order to maintain correct normal orientation.*
- Added `JTriangle.RayIntersect` method.
- Renamed `ConvexHullIntersection` to `CollisionManifold`.
- Modified support function for `BoxShape`.
### Jitter 2.6.3 (2025-05-17)
- Aligned rigid bodies (`RigidBodyData`) to a 64-byte boundary (reduce false sharing).
- Bugfix in speculative contacts.
### Jitter 2.6.2 (2025-05-06)
- Use **Generics** in `NarrowPhase.cs` (avoid boxing for structs implementing the `ISupportMappable` interface).
- Added special code paths in `Contact.cs` for static bodies (avoid unnecessary cache line invalidation).
- Added `PredictPosition`, `PredictOrientation` and `PredictPose` to `RigidBody`.
- Added `CreateFromAxisAngle` and `Normalize` methods in `JQuaternion`.
### Jitter 2.6.1 (2025-04-24)
- Bugfix in `TriangleEdgeCollisionFilter` for speculative contacts.
### Jitter 2.6.0 (2025-04-24)
- Added `SampleHull` and `MakeHull` to `ShapeHelper`.
- Fixed hill climbing getting stuck for `ConvexHullShape`s.
- Added SIMD support for `PointCloudShape`s.
- Added option to ignore degenerated triangles in `TriangleMesh`.
- Made thickness parameter mandatory in `FatTriangleShape`.
- Added Fisher-Yates shuffle to `DynamicTree.Optimize`.
- Optimized `TriangleEdgeCollisionFilter`.
### Jitter 2.5.9 (2025-04-17)
- Use `CollideEpsilon` 1e-5 in MPREPA.
- Fixed a bug in `ShardedDictionary`.
### Jitter 2.5.8 (2025-04-16)
- Fixed `DynamicTree.Optimize` messing up collision pairs.
- Refactored `SoftBody.cs`
- Improved `TriangleEdgeCollisionFilter`.
- Further reduced GC.
### Jitter 2.5.7 (2025-04-06)
- Fixed possible crash when dynamically making bodies static.
- Improved memory footprint and reduced GC.
- Added `Logger` as a replacement for `Trace`.
### Jitter 2.5.6 (2025-03-08)
- Fixed concurrency bug in `world.GetArbiter`.
### Jitter 2.5.5 (2025-03-02)
- Added implicit conversion operators for System.Numerics Vector3 and Quaternion.
- Replaced Trace.WriteLine with Trace.Information, Warning, Error.
### Jitter 2.5.4 (2025-02-08)
- Renamed `JAngle.Radiant` to `JAngle.Radian`.
- Renamed namespace `Jitter2.UnmanagedMemory` to `Jitter2.Unmanaged`.
- Fixed `body.AddShape(IEnumerable shapes)` for one-time-use iterators.
- Smaller improvements in XML-documentation.
### Jitter 2.5.3 (2025-01-12)
- DynamicTree, `Optimize` takes a delegate now.
- Fixed `TriangleShape` ray cast not returning a normalized normal.
- Removed the `CollisionHelper` class.
- Renamed `ActiveList` and `UnmanagedActiveList` to `PartitionedSet` and `PartitionedBuffer`, respectively.
- Various smaller improvements (ToString() overloads, IEquality\ implementations, XML-documentation)
### Jitter 2.5.2 (2025-01-08)
- Added enumeration method to `DynamicTree` and made `PairHashSet` internal.
- **Breaking Change:** Removed `UseFullEPASolver` option.
- Further improved simulation performance under high lock contention scenarios.
### Jitter 2.5.1 (2024-12-31)
- Bugfix in `PairHashSet`.
### Jitter 2.5.0 (2024-12-23)
- Better utilization of multi core systems.
- Bugfix in collision detection (possible NaN values).
### Jitter 2.4.9 (2024-12-18)
- Huge improvements for the `DynamicTree` implementation.
### Jitter 2.4.8 (2024-11-27)
- Add option to build in double precision mode.
- Made `Constraint` constructor public to allow for custom constraints.
### Jitter 2.4.7 (2024-11-18)
- **Breaking Change:** Dropped .NET6 support, added .NET9.
- SIMD for contacts.
- Contact manifold overflow fix.
- Changed default damping.
- Improved auxiliary contact points.
- Minor API changes.
### Jitter 2.4.6 (2024-10-28)
- **Breaking Change:** Jitter world is now constructed using World.Capacity.
- **Breaking Change:** World.RayCast moved to World.DynamicTree.RayCast.
- **Breaking Change:** Renamed NumberSubsteps to SubstepCount.
- Added split impulses. **Breaking Change:** SolverIterations property is now a tuple.
- Several smaller improvements in the API.
### Jitter 2.4.5 (2024-10-07)
- Added new methods to NarrowPhase: Distance and Overlap.
- **Breaking Change:** Renamed NarrowPhase.SweepTest to NarrowPhase.Sweep.
- **Breaking Change:** Renamed NarrowPhase.GJKEPA to NarrowPhase.Collision.
- Made PointTest, Raycast and SweepTest to use new SimplexSolver and SimplexSolverAB implementations.
- Fixed normal in GJKEPA for separating case.
### Jitter 2.4.4 (2024-09-14)
- Implemented fixes and workarounds for using Jitter with a debugger attached.
### Jitter 2.4.3 (2024-08-31)
- Correct corner case beeing wrong in MPR collision detection due to typo (bug fix).
- FatTriangleShape level did not properly take transformations into account (bug fix).
### Jitter 2.4.2 (2024-08-26)
- Added FatTriangleShape to give triangles thickness which can be useful for static triangle meshes.
- Removal from potential pairs in DynamicTree ignores filters from now on (bug fix).
- Use sweep tests for speculative contacts, vastly improving simulation quality in this scenario.
- **Breaking Change:** Redefinition of NarrowPhase.SweepTest results.
- Improved TriangleEdgeCollisionFilter.
### Jitter 2.4.1 (2024-08-21)
- Improved TriangleEdgeCollisionFilter.
- Implemented analytical box and sphere ray casting.
- Made Restitution and Friction public in Contact.
- Improved DynamicTree interface.
- Added Debug.Asserts in ActiveList.
- Marked ArbiterKey as readonly.
- Added enumerator to PairHashSet.
- Changed ShapeHelper.MakeHull to take a generic of type ICollection.
### Jitter 2.4.0 (2024-08-10)
- Improved TrimPotentialPairs logic.
- Optimized quaternion vector transformation.
- Extended functionality of ContactData.UsageMask.
- **Breaking Change:** Overhauled the shape system. Regular shapes (box, sphere, capsule, ...) now derive from RigidBodyShape. Some method signatures changed slightly, e.g. ray casting.
- Improved exceptions.
- Added ReferenceFrameAttribute.
### Jitter 2.3.1 (2024-06-02)
- ReadOnly wrappers (ReadOnlyList, ReadOnlyHashset) are now structs.
- Shapes with very small dimensions might have close to zero or zero mass/inertia. Creating rigid bodies from them now throws an exception (use body.AddShape(shape, setMassInertia: false) to not use the shape's mass properties).
- Added BeginCollide and EndCollide events per body.
### Jitter 2.3.0 (2024-05-20)
- Added RigidBody.RemoveShape overload to remove multiple shapes at once.
- Marked Rigid.ClearShapes deprecated.
- **Breaking Change:** Use JQuaternion for orientations. Sorry for the API break.
### Jitter 2.2.1 (2024-04-29)
- Add optional activate parameter to world.AddShape.
- Add NarrowPhase.SweepTest.
- EPA collision detection: various improvements.
- Improve exit condition for RayCast and PointTest.
- Remove redundant ArgumentException for zero mass shapes.
- Handle zero time steps. Throw ArgumentException for negative time steps.
- Add joint base class to joint classes.
### Jitter 2.2.0 (2024-01-02)
- **Breaking Change:** Renamed `Raycast` to `RayCast`.
- `world.Remove(world.NullBody)` does now remove all shapes, constraints and contacts associated with NullBody.
- `world.AddShape(shape)` respects the activation state of the associated rigid body. Most notable: performance improvement when directly adding `TriangleShape`s to world.NullBody for static geometry.
- Performance improvements for ConvexHullShape.
- Improved termination condition in GJKEPA collision detection.
### Jitter 2.1.1 (2023-12-17)
- Fixed O(n^2) problem in `TriangleMesh` due to hash collisions.
- `WorldBoundingBox` of `Shape` is now updated even if no `RigidBody` is attached.
### Jitter 2.1.0 (2023-12-10)
- Added debug drawing for rigid bodies (`RigidBody.DebugDraw`).
- Fixed a bug in `CalculateMassInertia` within `TransformedShape.cs`.
- Improved ray casting performance and introduced `NarrowPhase.PointTest`.
- **Breaking Change:** Inverted behavior of `BroadPhaseCollisionFilter`.
- **Breaking Change:** Inverted definition of damping factors in `RigidBody.Damping` (0 = no damping, 1 = immediate halt).
- Added `RigidBody.SetMassInertia` overload to enable setting the inverse inertia to zero.
- An exception is now thrown when a body's mass is set to zero.
- Fixed a bug in the friction handling in `Contact.cs`.
### Jitter 2.0.1 (2023-10-28)
- Fixed a bug in contact initialization which affected soft body physics.
### Jitter 2.0.0 (2023-10-22)
Initial stable Release.
================================================
FILE: docfx/docs/documentation/arbiters.md
================================================
# Arbiters
Arbiters manage contact information between two rigid bodies.
An arbiter is created when two shapes begin overlapping and is removed when they separate or when one of the involved bodies is removed from the world.
Each arbiter can hold up to four cached contact points (see `ContactData`).
## Arbiters and shapes
Arbiters are created between *shapes*, not bodies.
Since a rigid body can have multiple shapes attached to it, a single pair of bodies may produce multiple arbiters—one for each pair of overlapping shapes.
For example, consider two bodies where body A has two box shapes and body B has one sphere shape.
If both boxes overlap with the sphere, two separate arbiters are created: one for each box–sphere pair.
> [!NOTE]
> **One arbiter per shape pair**
> Even if the contact manifold changes over time, the arbiter remains the same as long as the two shapes stay in proximity.
> Contact points within the arbiter are cached and updated across frames.
## ArbiterKey
Each arbiter is identified by an `ArbiterKey`, which is an ordered pair of `ulong` identifiers.
For arbiters created by the engine, these identifiers are the `ShapeId` values of the two shapes involved.
The ordering is canonical: the shape with the smaller ID is always first.
```cs
public readonly struct ArbiterKey(ulong key1, ulong key2) : IEquatable
{
public readonly ulong Key1 = key1;
public readonly ulong Key2 = key2;
}
```
The order of `Key1` and `Key2` matters for equality comparison.
## Accessing contact data
The `Arbiter` class exposes a `Handle` property of type `JHandle`.
Through `arbiter.Handle.Data`, the underlying contact information can be read and modified:
```cs
ref ContactData data = ref arbiter.Handle.Data;
```
The `ContactData` structure contains up to four contact slots.
The `UsageMask` bitfield indicates which slots are active.
It also stores the combined `Friction` and `Restitution` coefficients for the contact pair (derived from the maximum of the two bodies' values).
> [!CAUTION]
> **Lifetime**
> The contact data is only valid while the arbiter is registered with the world.
> Do not cache references to `ContactData`.
> Accessing it after the arbiter has been removed results in undefined behavior.
## Collision events
The `RigidBody` class provides two events that supply the associated arbiter:
```cs
body.BeginCollide += (Arbiter arb) =>
{
// Called when a new arbiter is created involving this body.
};
body.EndCollide += (Arbiter arb) =>
{
// Called when an arbiter involving this body is removed.
};
```
These events can be used to inspect or modify contact properties.
For example, overriding friction for a specific contact:
```cs
body.BeginCollide += (Arbiter arb) =>
{
arb.Handle.Data.Friction = 0.0f;
};
```
Since arbiters are per shape pair, a body with multiple shapes may trigger `BeginCollide` multiple times for the same opposing body—once for each shape pair.
## Custom arbiters
Arbiters are not limited to the engine's built-in collision detection.
Custom arbiters can be created and managed using `world.GetOrCreateArbiter` and `world.RegisterContact`.
### Creating an arbiter
`GetOrCreateArbiter` takes two `ulong` identifiers and two rigid bodies.
If an arbiter for the given ID pair already exists, it is returned; otherwise, a new one is created:
```cs
world.GetOrCreateArbiter(id0, id1, body1, body2, out Arbiter arbiter);
```
The identifiers do not have to be shape IDs—they can be any `ulong` values that uniquely identify the contact pair.
This allows custom collision systems to create arbiters for application-specific purposes.
> [!WARNING]
> **ID ordering**
> The order of the two identifiers matters.
> `ArbiterKey(1, 2)` and `ArbiterKey(2, 1)` are considered different keys.
> When creating custom arbiters, be consistent with the ordering.
### Registering contacts
Once an arbiter exists, contacts can be registered into it:
```cs
world.RegisterContact(arbiter, point1, point2, normal);
```
All vectors must be in world space, and the normal must be a unit vector.
The `removeFlags` parameter can be used to exclude certain velocity components from the solver (e.g., `ContactData.SolveMode.Angular` for angular-only or linear-only responses).
Alternatively, contacts can be registered using IDs directly, which also creates the arbiter if needed:
```cs
world.RegisterContact(id0, id1, body1, body2, point1, point2, normal);
```
### Contact caching
An arbiter holds at most four contact points.
When a new contact is registered, the arbiter decides how to incorporate it:
1. **Slot available**: If fewer than four contacts are active, the new point is compared to existing contacts. If it is close enough to an existing contact (within a break threshold), it replaces that contact and inherits its accumulated impulse for warm-starting. Otherwise it is inserted into an empty slot as a new contact.
2. **All slots full**: When all four slots are occupied, the algorithm evaluates which existing contact to replace. For each of the four candidates, it computes the area of the quadrilateral formed by the new point and the three remaining contacts. The candidate whose removal maximizes this area is replaced. This heuristic keeps contacts well-distributed across the contact patch, which produces the most stable contact manifold.
Contacts that drift apart beyond a distance threshold during position integration are automatically removed from the manifold.
## Arbiter lifecycle
1. **Creation**: When two shapes begin overlapping, the engine calls `GetOrCreateArbiter` using the shape IDs as keys. The `BeginCollide` event is raised on both bodies.
2. **Update**: Each `world.Step`, contact positions are updated. Contacts that have separated beyond a threshold are removed from the arbiter. New contacts from ongoing collisions are added.
3. **Removal**: When all contacts in an arbiter are broken and the shapes are no longer overlapping, the arbiter is removed. The `EndCollide` event is raised on both bodies. The arbiter is returned to an internal pool for reuse.
================================================
FILE: docfx/docs/documentation/bodies.md
================================================
# Rigid Bodies
Rigid bodies are the main entity of the dynamics system.
## Creating a body
Rigid bodies are associated with an instance of the `World` class:
```cs
var world = new World();
var body = world.CreateRigidBody();
```
## Adding shapes
Multiple shapes can be added to a rigid body, for example:
```cs
body.AddShapes([new SphereShape(radius: 2), new BoxShape(size: 1)]);
```
Shapes determine how bodies collide with each other.
> [!WARNING]
> **Creating many bodies at once**
> When calling body.AddShape(shape), the shape is registered in the collision system of the engine and immediately added to the spatial tree structure (`DynamicTree`) for efficient broad-phase collision detection.
> Registering many objects at $(0, 0, 0)$ must be prevented by specifying the rigid body position, before adding shapes.
> [!WARNING]
> **Passing the same instance to multiple bodies**
> Passing the same instance of a shape to multiple bodies is not allowed in Jitter and will throw an exception.
The sphere shape is defined so that its geometric center aligns with the (local) coordinate system's center at $(0, 0, 0)$.
The same holds for all basic primitives (sphere, box, capsule, cone, cylinder).
After adding a shape, the mass properties (mass and inertia) of the associated rigid body are calculated accordingly.
Unit density is assumed for the calculations.
Adding just a sphere
```cs
body.AddShape(new SphereShape(radius: 1));
```
will result in a body with the textbook inertia and mass of a unit-density sphere of radius one.
The mass properties of the body can also be set directly using `body.SetMassInertia`.
Passing `MassInertiaUpdateMode.Preserve` to `body.AddShape(...)` or `body.AddShapes(...)` prevents the automatic recalculation of mass properties when shapes are added.
***The position of the rigid body has to align with the center of mass.**
So in the local reference frame of the body, the center of mass is $(0, 0, 0)$. Shapes or combinations of shapes must be translated accordingly.*
### Debugging shapes
The `RigidBody` class offers the `body.DebugDraw(IDebugDrawer drawer)` method which creates a triangle hull for each shape added to the body and calls the `drawer.DrawTriangle` method in the provided `IDebugDrawer` implementation.
The coordinates of the triangles are in world space and can be drawn to debug the collision shape of the rigid body.
> [!WARNING]
> **body.DebugDraw performance**
> Every call to `body.DebugDraw` the triangle hulls are generated on the fly.
> Since this is a slow operation the method should only be called for debugging purposes.
## Forces and impulses
Forces and torques can be applied using `body.AddForce`. Forces accumulate over the current step and are reset after integration.
For instantaneous velocity changes, use `body.ApplyImpulse`:
```cs
// Linear impulse — changes velocity immediately.
body.ApplyImpulse(new JVector(0, 10, 0));
// Impulse at a world-space position — changes both linear and angular velocity.
body.ApplyImpulse(new JVector(0, 10, 0), hitPosition);
```
Both overloads accept an optional `wakeup` parameter (default `true`).
When set to `false`, the impulse is silently ignored if the body is sleeping.
## Gravity
The gravity for the world can be set using `world.Gravity`.
The property `body.AffectedByGravity` can be used to disable gravity for individual bodies.
## Damping
Jitter2 uses a simple damping system to slow rigid bodies down.
This improves simulation stability and also resembles mechanical systems losing energy in the real world.
There is a linear and an angular damping factor for each body which can be set using `body.Damping`.
With each `world.Step`, the angular and linear velocity of each rigid body is multiplied by $1-\gamma$, where $\gamma$ is the damping factor.
For performance reasons there is no time dependency for the damping system.
As a result, bodies in a simulation with smaller timesteps experience greater damping.
## Speculative contacts
Speculative contacts can be utilized to prevent fast and small objects from tunneling through thin objects.
An object moving quickly enough might 'miss' a collision since the distance traveled between two frames exceeds the thickness of another object.
Speculative contacts can be enabled on a per-body basis using `body.EnableSpeculativeContacts`.
The `world.SpeculativeRelaxationFactor` and `world.SpeculativeVelocityThreshold` can be adjusted to fine-tune speculative contacts for specific use cases.
However, it should be noted that an accurate simulation of fast-moving objects is only possible using smaller time steps.
Speculative contacts may involve a trade-off of less accurate collision detection and response.
## Friction and Restitution
Friction and restitution coefficients may be set through `body.Friction` and `body.Restitution`.
For a collision of two bodies with different coefficients the maximum value of each body is taken.
## Collide events
`RigidBody` provides two events: `BeginCollide` and `EndCollide`.
These events are triggered whenever an arbiter is created or removed which involves the rigid body.
See [Arbiters](arbiters.md#collision-events) for details and examples.
## Activation/Deactivation
A rigid body is always assigned to an island.
Islands are formed by bodies which are pairwise interacting with each other through contacts or constraints.
Different islands are not interacting with each other in any way.
Active rigid bodies may be marked for deactivation by the world once their angular and linear velocity remain below the thresholds defined in `body.DeactivationThreshold` for a period defined by `body.DeactivationTime`.
If all bodies within an island are marked for deactivation the whole island gets deactivated.
The simulation cost for inactive bodies is effectively zero.
Islands (and their associated bodies) may get woken up as soon as a collision with an active body is registered.
Using `body.SetActivationState`, the user can reset the internal deactivation time clock for the rigid body.
It will not immediately change the activation state of the body (`body.IsActive`).
The next `world.Step` will then consider this body and its connected island for activation or deactivation.
Calling e.g. `body.SetActivationState(false)` on a falling body with a velocity greater than `body.DeactivationThreshold` will have no effect.
## Static bodies
Static bodies (`body.MotionType == MotionType.Static`) have infinite mass and therefore are not affected by collisions or constraints.
They also do not join islands.
Static bodies do not generate collisions with other static or inactive bodies.
Because of this, the position of static bodies should not be altered while in contact with other bodies.
## Kinematic bodies
Kinematic bodies (`body.MotionType == MotionType.Kinematic`) can have a velocity and therefore change their position.
They act similar to static bodies during collisions—their velocity is not changed when colliding with a regular body.
They do take part in collision islands.
================================================
FILE: docfx/docs/documentation/constraints.md
================================================
# Constraints
Constraints restrict degrees of freedom of rigid bodies.
In Jitter2, constraints always act between two bodies.
A joint is a collection of multiple constraints, so it may act on multiple bodies.
## Degrees of Freedom
A rigid body has 6 degrees of freedom (DOF): 3 translational and 3 rotational.
Constraints remove one or more of these degrees of freedom.
| Constraint | Removed DOF | Description |
|------------|-------------|-------------|
| `BallSocket` | 3 translational | Anchors a point on each body together |
| `FixedAngle` | 3 rotational | Locks relative orientation between bodies |
| `HingeAngle` | 2 rotational (3 with limit) | Allows rotation around a single axis only, optionally enforces angular limits |
| `PointOnLine` | 2 translational (3 with limit) | Constrains a point to a line, optionally limits distance along the axis |
| `PointOnPlane` | 1 translational (when limit active) | Constrains a point to a plane, only enforced when the limit is violated |
| `DistanceLimit` | 1 translational (when limit active) | Constrains the distance between anchor points, only enforced when outside the limit range |
| `TwistAngle` | 1 rotational (when limit active) | Limits relative twist around an axis, only enforced when outside the limit range |
| `ConeLimit` | 1 rotational (when limit active) | Limits angular tilt between two axes, only enforced when outside the limit range |
| `AngularMotor` | — | Drives angular velocity (does not remove DOF) |
| `LinearMotor` | — | Drives linear velocity (does not remove DOF) |
| `SpringConstraint` | 1 translational | Spring-like force along the anchor connection |
## Joints
Joints are combinations of constraints for common use cases:
| Joint | Constraints used | Remaining DOF |
|-------|------------------|---------------|
| `HingeJoint` | `BallSocket` + `HingeAngle` (+ optional `AngularMotor`) | 1 rotational |
| `PrismaticJoint` | `PointOnLine` + `FixedAngle` or `HingeAngle` (+ optional `LinearMotor`) | 1 translational |
| `UniversalJoint` | `BallSocket` + `TwistAngle` (+ optional `AngularMotor`) | 2 rotational |
| `WeldJoint` | `BallSocket` + `FixedAngle` | 0 (fully locked) |
## Creating constraints
Constraints are instantiated through a create method in the world class:
```cs
var constraint = world.CreateConstraint(body1, body2);
```
Jitter2 allocates and assigns unmanaged memory to constraints internally, allowing them to be used like regular classes.
The solver interacts with unmanaged memory structures of type `ConstraintData` (256 bytes) or `SmallConstraintData` (128 bytes), depending on whether `constraint.IsSmallConstraint` is set.
Small constraints are designed for simpler constraints with a smaller memory footprint, such as spring constraints for soft bodies.
> [!CAUTION]
> **Constraint Initialization**
> For all default constraints, `constraint.Initialize` must be called once after `world.CreateConstraint`.
### World space initialization
Constraints are initialized using world space coordinates (anchor points, axes).
The bodies should be positioned in a valid configuration before calling `Initialize`.
The constraint then computes and stores local reference frames for each body based on their current poses.
This means the constraint "remembers" the initial setup and will try to maintain it during simulation.
### Example: Hinge joint
A swinging door can be implemented by combining two constraints: A `HingeAngle` constraint which removes two degrees of angular freedom (the bodies can only rotate around a single axis), and a `BallSocket` constraint removing all three translational degrees of freedom:
```cs
var hingeAngle = world.CreateConstraint(body1, body2);
hingeAngle.Initialize(hingeAxis, AngularLimit.Full);
var ballSocket = world.CreateConstraint(body1, body2);
ballSocket.Initialize(hingeCenter);
```
Alternatively, the `HingeJoint` can be used for convenience:
```cs
var hinge = new HingeJoint(world, body1, body2, hingeCenter, hingeAxis);
```
### Fixed constraints
To constrain a rigid body relative to world space, use `world.NullBody` as one of the bodies.
For example, to keep a capsule upright:
```cs
var upright = world.CreateConstraint(capsule, world.NullBody);
upright.Initialize(JVector.UnitY, AngularLimit.Full);
```
### Softness and Bias
For most constraints, softness and bias values can be set.
These values define how strictly the constraint limits are enforced.
Softer constraints may improve simulation stability but do not fully enforce the constraint limits.
The softness and bias parameters can be tweaked for optimal results.
Better constraint behavior can also be achieved by sub-stepping, see [World](world.md).
### Enable/Disable constraints
Constraints can be temporarily enabled or disabled using `constraint.IsEnabled`.
================================================
FILE: docfx/docs/documentation/dynamictree.md
================================================
# Dynamic Tree
The dynamic tree holds instances which implement the `IDynamicTreeProxy` interface.
Its main task is to efficiently determine if a proxy's axis-aligned bounding box overlaps with the axis-aligned bounding box of any other proxy in the world.
A naive implementation requires $\mathcal{O}\left(n\right)$ operations (checking for an overlap with every of the $n-1$ entities).
The tree structure accelerates this to $\mathcal{O}\left(\mathrm{log}(n)\right)$.
Since proxies are dynamic and can move, the tree must be continuously updated.
To trigger updates less frequently, entities are enclosed within slightly larger bounding boxes than their actual size.
This bounding box extension is defined by the `Velocity` property of the `IDynamicTreeProxy` interface.

## Adding proxies
All shapes added to a rigid body (`body.AddShape`) are automatically registered with `world.DynamicTree`.
Custom implementations of `IDynamicTreeProxy` can be added to the tree using `tree.AddProxy`.
In this case, a `BroadPhaseFilter` must be implemented and registered (using `world.BroadPhaseFilter`) to handle collisions with the custom proxy, otherwise an `InvalidCollisionTypeException` is thrown.
## Enumerate Overlaps
The tree implementation needs to be updated using `tree.Update`.
This is done automatically for the dynamic tree owned by the world class (`world.DynamicTree`).
Internally, `UpdateWorldBoundingBox` is called for active proxies implementing the `IUpdatableBoundingBox` interface, and the internal book-keeping of overlapping pairs is updated.
Overlaps can be queried using `tree.EnumerateOverlaps`.
## Querying the tree
The dynamic tree is a broadphase structure.
Its job is to quickly reduce the set of possible candidates.
Some APIs stop there and return proxies, while others continue with an exact narrowphase test.
In practice there are two layers of queries:
- Broadphase-only queries, such as `Query(...)`, which return candidate proxies.
- Broadphase + narrowphase queries, such as `RayCast(...)`, `SweepCast(...)`, and `FindNearest(...)`, which continue with exact tests on supported proxies.
### Broadphase-only queries
All tree proxies that overlap a given axis-aligned box can be queried:
```cs
public void Query(T hits, in JBoundingBox box) where T : class, ICollection
public void Query(ref TSink hits, in JBoundingBox box) where TSink : ISink
```
As well as all proxies which overlap with a ray:
```cs
public void Query(T hits, in JVector rayOrigin, in JVector rayDirection) where T : class, ICollection
public void Query(ref TSink hits, in JVector rayOrigin, in JVector rayDirection) where TSink : ISink
```
Custom broadphase queries can easily be implemented.
An implementation which queries all proxies overlapping with a single point:
```cs
var stack = new Stack();
stack.Push(tree.Root);
ReadOnlySpan nodes = tree.Nodes;
while (stack.TryPop(out int id))
{
ref readonly DynamicTree.Node node = ref nodes[id];
if (node.ExpandedBox.Contains(point))
{
if (node.IsLeaf)
{
Console.WriteLine($'{node.Proxy} contains {point}.');
}
else
{
stack.Push(node.Left);
stack.Push(node.Right);
}
}
}
```
### Exact query helpers
The tree also provides helpers that combine broadphase pruning with an exact narrowphase test.
These return the closest exact hit instead of raw candidate proxies.
All exact query helpers return `true` on success and `false` when nothing is found.
#### Ray casting
All proxies implementing `IRayCastable` can be ray cast, including all shapes:
```cs
// Unbounded
public bool RayCast(JVector origin, JVector direction, RayCastFilterPre? pre, RayCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector normal, out Real lambda)
// Bounded: only considers hits with lambda ≤ maxLambda
public bool RayCast(JVector origin, JVector direction, Real maxLambda, RayCastFilterPre? pre, RayCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector normal, out Real lambda)
```
The pre- and post-filters can be used to discard hits during the ray cast.
A ray is shot from the origin into the specified direction.
Returns `true` if a hit is found, including the case where the ray origin is inside the hit shape (in which case `lambda` is zero and `normal` is `JVector.Zero`).
The point of collision is given by `hit = origin + lambda * direction`.
#### Sweep casting
All proxies implementing `ISweepTestable` can be sweep-tested, including all shapes:
```cs
// Unbounded
public bool SweepCast(in T support, in JQuaternion orientation, in JVector position, in JVector direction,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda)
where T : ISupportMappable
// Bounded: only considers hits with lambda ≤ maxLambda
public bool SweepCast(in T support, in JQuaternion orientation, in JVector position, in JVector direction, Real maxLambda,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda)
where T : ISupportMappable
```
The query shape is defined by an `ISupportMappable` plus its world-space `orientation` and `position`.
`direction` is the sweep direction vector; `lambda` is expressed in units of `direction`, so the query shape's center at impact is at `position + lambda * direction`.
`pointA` and `pointB` are the contact points at the sweep origin (before any displacement is applied).
Because sweep cast targets are always stationary, `pointB` is simply the world-space contact point.
The world-space contact point on the query shape at impact is `pointA + lambda * direction`.
`normal` is the collision normal in world space.
Returns `true` if a hit is found, including the case where the shapes already overlap (in which case `lambda` is zero and `normal` is `JVector.Zero`).
The **pre-filter** (`SweepCastFilterPre`) is called before the exact narrowphase test and receives only the candidate proxy.
Use it to cheaply skip proxies by identity (e.g. ignore the body being swept).
The **post-filter** (`SweepCastFilterPost`) is called after the exact test and receives the full `SweepCastResult` (including `Lambda`, `Normal`, `PointA`, `PointB`).
Use it to reject hits based on geometry, or to collect multiple results rather than stopping at the first.
Returning `false` from either filter skips that candidate without terminating the sweep.
For common built-in query shapes, convenience overloads are available:
```cs
public bool SweepCastSphere(Real radius, in JVector position, in JVector direction, ...)
public bool SweepCastBox(in JVector halfExtents, in JQuaternion orientation, in JVector position, in JVector direction, ...)
public bool SweepCastCapsule(Real radius, Real halfLength, in JQuaternion orientation, in JVector position, in JVector direction, ...)
public bool SweepCastCylinder(Real radius, Real halfHeight, in JQuaternion orientation, in JVector position, in JVector direction, ...)
```
Each has a bounded variant with a `maxLambda` parameter.
#### Find nearest query
All proxies implementing `IDistanceTestable` can be distance-queried, including all shapes:
```cs
// Unbounded: considers all IDistanceTestable proxies in the tree
public bool FindNearest(in T support, in JQuaternion orientation, in JVector position,
FindNearestFilterPre? pre, FindNearestFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real distance)
where T : ISupportMappable
// Bounded: only considers proxies closer than maxDistance
public bool FindNearest(in T support, in JQuaternion orientation, in JVector position, Real maxDistance,
FindNearestFilterPre? pre, FindNearestFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real distance)
where T : ISupportMappable
```
The query shape is defined by an `ISupportMappable` plus its world-space `orientation` and `position`.
`pointA` and `pointB` are the closest points on the query shape and the found proxy respectively.
`normal` is the unit direction from the query shape toward the found proxy.
Returns `true` if a proxy is found, including the case where the query shape already overlaps a proxy (in which case `distance` is zero and `normal` is `JVector.Zero`).
Returns `false` only when no proxy is found within range.
The **pre-filter** (`FindNearestFilterPre`) is called before the exact narrowphase test and receives only the candidate proxy.
The **post-filter** (`FindNearestFilterPost`) is called after the exact test and receives the full `FindNearestResult`.
For overlap results the post-filter receives `distance = 0` and `normal = Zero`.
Returning `false` from either filter skips that candidate and continues the search.
To skip overlapping proxies and find the nearest *separated* proxy instead, reject overlap results in the post-filter:
```cs
world.DynamicTree.FindNearestSphere(radius, position,
pre: null,
post: result => result.Distance > 0,
out proxy, out pointA, out pointB, out normal, out distance);
```
For common built-in query shapes, convenience overloads are available:
```cs
public bool FindNearestPoint(in JVector position, ...)
public bool FindNearestSphere(Real radius, in JVector position, ...)
```
Each has a bounded variant with a `maxDistance` parameter.
### Overlap check example
Overlap checks can be composed by querying the tree with a bounding box first and then running an exact narrowphase overlap test against the returned candidates:
```cs
// Returns all RigidBodyShapes overlapping a sphere at 'center' with 'radius'.
static IEnumerable OverlapSphere(World world, JVector center, float radius)
{
var queryBox = new JBoundingBox(
center - new JVector(radius),
center + new JVector(radius));
var candidates = new List();
world.DynamicTree.Query(candidates, queryBox);
var querySphere = SupportPrimitives.CreateSphere(radius);
foreach (var proxy in candidates)
{
if (proxy is not RigidBodyShape shape) continue;
ref var data = ref shape.RigidBody.Data;
bool hit = NarrowPhase.Overlap(
querySphere,
shape,
JQuaternion.Identity,
data.Orientation,
center,
data.Position);
if (hit) yield return shape;
}
}
```
This pattern is usually enough for overlap checks and avoids introducing additional broadphase API surface.
================================================
FILE: docfx/docs/documentation/filters.md
================================================
# Collision Filters
There are three types of collision filters: `world.DynamicTree.Filter`, `world.BroadPhaseFilter`, and `world.NarrowPhaseFilter`.
## Dynamic tree filter
The `world.DynamicTree.Filter`
```cs
public Func Filter { get; set; }
```
is the earliest filter applied during `world.Step` and is set by default to `World.DefaultDynamicTreeFilter`:
```cs
public static bool DefaultDynamicTreeFilter(IDynamicTreeProxy proxyA, IDynamicTreeProxy proxyB)
{
if (proxyA is RigidBodyShape rbsA && proxyB is RigidBodyShape rbsB)
{
return rbsA.RigidBody != rbsB.RigidBody;
}
return true;
}
```
This filters out collisions between shapes that belong to the same body.
The dynamic tree will ignore these collisions, and no potential pairs will be created.
For soft bodies, another collision filter is typically used (defined in `SoftBodies.DynamicTreeCollisionFilter.Filter`), which also filters out collisions between shapes belonging to the same soft body.
## Broad phase filter
By default `world.BroadPhaseFilter`
```cs
public IBroadPhaseFilter? BroadPhaseFilter { get; set; }
```
is `null`. It is used to filter out collisions that passed broad phase collision detection—that is, after the `DynamicTree` has added the collision to the `PotentialPair` hash set.
This can be useful if custom collision proxies are added to `world.DynamicTree`.
Since the world only knows how to handle collisions between `RigidBodyShape`s, a filter must handle the detected collision (implement custom collision response code and filter out the collision) such that no `InvalidCollisionTypeException` is thrown.
The soft body implementation is based on this kind of filter (see `SoftBodies.BroadPhaseCollisionFilter`).
### Example: Collision groups
Collision groups can be implemented using a broad phase filter.
In this example, there are two teams: team blue and team red.
A filter that disregards all collisions between team members of different colors:
```cs
public class TeamFilter : IBroadPhaseFilter
{
public class TeamMember { }
public static TeamMember TeamRed = new();
public static TeamMember TeamBlue = new();
public bool Filter(Shape shapeA, Shape shapeB)
{
if (shapeA.RigidBody.Tag is not TeamMember || shapeB.RigidBody.Tag is not TeamMember)
{
// Handle collision normally if at least one body is not a member of any team
return true;
}
// There is no collision between team red and team blue.
return shapeA.RigidBody.Tag == shapeB.RigidBody.Tag;
}
}
```
The `TeamFilter` class can then be instantiated and assigned to `world.BroadPhaseFilter`:
```cs
world.BroadPhaseFilter = new TeamFilter();
...
bodyA.Tag = TeamFilter.TeamBlue;
bodyB.Tag = TeamFilter.TeamRed;
bodyC.Tag = TeamFilter.TeamRed;
```
## Narrow phase filter
The `world.NarrowPhaseFilter`
```cs
public INarrowPhaseFilter? NarrowPhaseFilter { get; set; }
```
operates similarly.
However, this callback is called after narrow phase collision detection, meaning detailed collision information (such as normal, penetration depth, and collision points) is available at this stage.
The filter can not only exclude collisions but also modify collision information.
The default narrow phase collision filter is assigned to an instance of `TriangleEdgeCollisionFilter`, which filters out so-called 'internal edges' for `TriangleShape`s.
These internal edges typically cause collision artifacts when rigid bodies slide over the edges of connected triangles forming static geometry.
This problem is also known as 'ghost collisions'.
================================================
FILE: docfx/docs/documentation/general.md
================================================
# General
This section covers fundamental design decisions and configuration options.
## Collision Detection Philosophy
Jitter2 takes a unified approach to collision detection that differs from many other physics engines. Unlike engines that implement dedicated algorithms for specific shape pairs (sphere-sphere, box-box, capsule-capsule, etc.), Jitter2 treats all collision detection uniformly using implicit shapes. Every shape is represented through a support function, and collisions are resolved via MPR (Minkowski Portal Refinement), falling back to EPA (Expanding Polytope Algorithm) for deep penetrations. This design simplifies the codebase and makes it straightforward to add custom shapes—any shape that provides a support mapping automatically works with all other shapes.
Traditional physics engines use a three-phase collision pipeline: broad phase (spatial partitioning), mid-phase (hierarchical bounding volumes for complex meshes), and narrow phase (exact shape intersection). Jitter2 eliminates the mid-phase entirely. Instead of building internal acceleration structures for complex geometry, Jitter2 relies on [collision filters](filters.md) to handle large-scale environments. This enables user-defined spatial partitioning strategies—heightmaps, detailed triangle meshes, or even infinite voxel worlds can be implemented by generating collision geometry on-demand within filter callbacks.
## Precision
Jitter2 supports both single-precision (`float`) and double-precision (`double`) floating-point arithmetic, selected at compile time. To build with double precision, either uncomment `#define USE_DOUBLE_PRECISION` in [Precision.cs](https://github.com/notgiven688/jitterphysics2/blob/main/src/Jitter2/Precision.cs), or use the command line option:
```bash
dotnet build -c Release -p:DoublePrecision=true
```
The active precision mode can be checked at runtime via `Precision.IsDoublePrecision`. In single precision, `JVector.X` is a `float`; in double precision, it is a `double`.
## Deterministic Simulation
Jitter2 provides an optional cross-platform deterministic solver mode via :
```cs
world.SolveMode = SolveMode.Deterministic;
```
This mode is intended for reproducible simulation across platforms when the same Jitter2 version, precision mode, and stepping inputs are used. It is useful for automated tests, replay systems, debugging, and lockstep-style simulation.
The important requirement is not that the entire world matches. What matters is that each interacting island is assembled in the same order: the bodies, shapes, and constraints that participate in that island must be added in the same sequence. If the same island is created in the same order and receives the same inputs, it will evolve the same even if unrelated parts of the world were built differently.
The deterministic path keeps contacts and constraints in a stable order and uses internal stable trigonometric helpers (`StableMath`) so critical math does not depend on platform-specific `Math`/`MathF` behavior.
| Configuration | What is guaranteed | What must match | What is not guaranteed |
| --- | --- | --- | --- |
| `SolveMode.Deterministic` | Cross-platform reproducible simulation results, independent of threading mode and internal SIMD/scalar execution path | Same Jitter2 version, same precision mode, same step sequence / fixed time step, same order in which the participating bodies, shapes, and constraints are added within each interacting island | Float and double matching each other, results across different engine versions |
| `SolveMode.Regular` with `multiThread: false` | Reproducibility only in the narrow sense that the exact same world build can produce the same result again within the same .NET process | Exact same construction path, exact same addition order, same step sequence, same .NET process / runtime run | Cross-platform determinism, rebuilding the same final scene through a different history, matching results across different process launches |
| `SolveMode.Regular` with `multiThread: true` | No deterministic guarantee | None | Reproducible ordering or cross-platform bit identity |
> [!WARNING]
> To the best of our current knowledge, this feature is cross-platform deterministic for the cases described above, but the claim is based on the current implementation and test coverage. At the time of writing, CI exercises the deterministic hash test on `ubuntu-latest`, `windows-latest`, and `macos-latest`, which currently correspond to x64 Linux, x64 Windows, and arm64 macOS on GitHub-hosted runners.
`SolveMode.Deterministic` can be significantly slower than `SolveMode.Regular`, which remains the default and recommended option for normal gameplay or interactive sandbox scenes.
## Coordinate System
Jitter2 uses a right-handed coordinate system.
The engine itself is coordinate-system agnostic and does not enforce any particular axis convention.
The only default that assumes a specific orientation is `World.Gravity`, which is initialized to `(0, -9.81, 0)`, treating the Y-axis as up.
This can be changed:
```cs
world.Gravity = new JVector(0, 0, -9.81f); // Z-up convention
```
## Tracing and Logging
Jitter2 provides mechanisms for logging and performance profiling.
### Logging
The `Logger` class provides a simple logging interface with three severity levels: `Information`, `Warning`, and `Error`.
To receive log messages, register a listener:
```cs
Logger.Listener = (level, message) =>
{
Console.WriteLine($"[{level}] {message}");
};
```
The engine logs warnings for events such as EPA convergence issues or memory allocation fallbacks.
### Performance Tracing
When compiled with the `PROFILE` symbol, Jitter2 records detailed timing information for each simulation phase (broad phase, narrow phase, solver iterations, etc.).
The trace data is stored in thread-local buffers for minimal overhead.
To export the recorded data:
```cs
Tracer.WriteToFile("trace.json");
```
The output file uses the Chrome Trace Event format and can be visualized in:
- `chrome://tracing` (paste in Chrome's address bar)
- [Perfetto UI](https://ui.perfetto.dev/)
When `PROFILE` is not defined, all tracing calls are completely stripped by the compiler via `[Conditional]` attributes, resulting in zero runtime overhead.
## Custom Math Types
Jitter2 defines its own math types (`JVector`, `JMatrix`, `JQuaternion`, `JBoundingBox`) rather than using `System.Numerics`. This allows precision to be switched globally between `float` and `double` without code changes, gives explicit control over memory layout using `[StructLayout(LayoutKind.Explicit)]`, and avoids dependencies on external math library behavior.
The explicit field offsets guarantee a predictable memory layout, enabling zero-copy conversion to and from other libraries' types:
```cs
// Convert to any layout-compatible type
MyVector3 myVec = jitterVector.UnsafeAs();
// Convert from any layout-compatible type
JVector jitterVector = JVector.UnsafeFrom(myVec);
```
For convenience, implicit conversions to and from `System.Numerics` types are also provided:
```cs
System.Numerics.Vector3 sysVec = jitterVector; // implicit conversion
JVector jitterVec = sysVec; // implicit conversion
```
These conversions involve copying and potential precision loss when converting from double to float.
### Vector and matrix convention
Jitter2 treats vectors as **column vectors**.
A vector $v$ is transformed by a matrix $M$ using $M \cdot v$ (post-multiplication):
```cs
JVector result = JVector.Transform(v, M); // computes M * v
```
This is the standard convention used in mathematics, physics, and most graphics APIs (e.g., OpenGL, Vulkan/GLSL).
`System.Numerics` uses the opposite convention: vectors are **row vectors** and transformation is written as $v \cdot M$ (pre-multiplication).
As a consequence, when converting transformation matrices between Jitter2 and `System.Numerics`, the matrices must be transposed.
Jitter2's `JMatrix` also stores its elements in column-major order in memory (i.e., `M11`, `M21`, `M31` are contiguous), whereas `System.Numerics.Matrix4x4` uses row-major storage.
================================================
FILE: docfx/docs/documentation/shapes.md
================================================
# Shapes
Shapes define how the rigid body collides with other objects.
Shapes implement the `ISupportMappable` interface and are always convex.
They can be passed to static methods defined in the `NarrowPhase` class for collision detection.
Shapes also implement the `IDynamicTreeEntry` interface and can be added to the `DynamicTree` class.
When a shape is added to a rigid body this is done automatically (`world.DynamicTree`).
> [!NOTE]
> **Concave Shapes**
> A concave shape can be represented by combining multiple convex shapes on a single rigid body.
> Third-party libraries for 'convex decomposition' can be used to generate convex hulls from arbitrary meshes.
## Default types
The inheritance hierarchy for the default shapes:
```text
Shape
├── RigidBodyShape
│ ├── BoxShape
│ ├── CapsuleShape
│ ├── ConeShape
│ ├── ConvexHullShape
│ ├── CylinderShape
│ ├── PointCloudShape
│ ├── TransformedShape
│ └── TriangleShape
└── SoftBodyShape
├── SoftBodyTetrahedron
└── SoftBodyTriangle
```
Most shapes are self-explanatory (additional details are in the API documentation), while some specifics are outlined below.
### ConvexHullShape
The constructor of the `ConvexHullShape` takes a list of triangles.
```cs
public ConvexHullShape(List triangles)
```
The triangles provided *must* form a convex hull.
The validity of the convex shape is not checked.
Invalid shapes can lead to glitched
collisions and/or non-terminating algorithms during collision detection.
The triangles are used to construct an internal acceleration structure that speeds up collision detection for this shape through hill-climbing.
The `convexHullShape.Clone()` method can be used to clone the shape:
The internal data structure is then used for both shapes.
### PointCloudShape
The `PointCloudShape` is very similar to the `ConvexHullShape`.
The constructor takes a list of vertices.
```cs
public PointCloudShape(List vertices)
```
The vertices do not need to form a convex hull; however, collision detection will 'shrink-wrap' these vertices, so the final collision shape is convex.
For example, passing the 8 vertices of a cube to the constructor generates a cube shape; adding a 9th vertex at the cube's center has no effect.
> [!WARNING]
> **Number of vertices**
> `PointCloudShape`s should only be used for a small to moderate number of vertices ($\approx{}300$). Larger numbers of vertices can negatively impact performance.
> `ConvexHullShape`s are the better choice for more complex hulls.
### TransformedShape
The `TransformedShape` takes another shape as input and transforms it.
```cs
public TransformedShape(RigidBodyShape shape, in JVector translation, in JMatrix transform)
```
Any affine transformation is possible.
The wrapped shape might be translated, rotated, scaled and sheared.
For example, a sphere shape could be transformed into an ellipsoid.
### TriangleShape
The `TriangleShape` has no volume.
It is mostly used for static geometry, although it can be added to non-static bodies.
The `TriangleShape` is constructed with a `TriangleMesh` and an index.
```cs
public TriangleShape(TriangleMesh mesh, int index)
```
A `TriangleMesh.Triangle` stores information about neighbour triangles.
This information is used in the `TriangleEdgeCollisionFilter` (enabled by default) to resolve collision artifacts that occur when shapes slide over the edges between connected triangles.
These edges are often referred to as 'internal edges' and can cause major problems when adding level geometry to a game.
### SoftBodyShape
The vertices of the `SoftBodyShape` are represented by rigid bodies.
The shapes (triangle and tetrahedron) are dynamically defined by the position of the vertices.
A `SoftBodyShape` is not added to a body.
## Custom shapes
Custom shapes can easily be implemented.
A shape is defined by its support function—which can be looked up or derived.
The following example demonstrates implementing a half-sphere (symmetry axis aligned with the y-axis) with a radius of one:
```cs
public class HalfSphereShape : RigidBodyShape
{
public override void SupportMap(in JVector direction, out JVector result)
{
const float centerOfMassOffset = 3.0f / 8.0f;
if (direction.Y >= 0.0f)
{
result = JVector.Normalize(direction);
}
else
{
JVector pDir = new JVector(direction.X, 0.0f, direction.Z);
float pDirSq = pDir.LengthSquared();
if (pDirSq < 1e-12f) result = JVector.Zero;
else result = (1.0f / MathF.Sqrt(pDirSq)) * pDir;
}
// shift, such that (0, 0, 0) is the center of mass
result.Y -= centerOfMassOffset;
}
public override void GetCenter(out JVector point)
{
point = JVector.Zero;
}
}
```
Mass properties and bounding boxes are automatically calculated from the support map using methods in the `ShapeHelper` class.
Performance can be optimized by providing overrides directly in the shape class:
```cs
public override void CalculateBoundingBox(in JQuaternion orientation, in JVector position, out JBoundingBox box)
public override void CalculateMassInertia(out JMatrix inertia, out JVector com, out float mass)
public override bool LocalRayCast(in JVector origin, in JVector direction, out JVector normal, out float lambda)
```
The `ShapeHelper` class can also be used to generate a triangle mesh representation of the shape (or any class implementing `ISupportMappable`):
```cs
public static List Tessellate(ISupportMappable support, int subdivisions = 3)
```
================================================
FILE: docfx/docs/documentation/world.md
================================================
# Jitter World
The `World` class contains all entities in the physics simulation and provides the `World.Step` method to advance the simulation by a single time step.
## World.Step
Forward the world by a single time step using
```cs
Step(float dt, bool multiThread = true)
```
### Time step size
> [!NOTE]
> **Units in Jitter2**
> The unit system is not explicitly defined.
> The engine is optimized for objects with a size of 1 [len_unit].
> For example, the collision system uses length thresholds on the order of 1e-04 [len_unit].
> It assumes a unit density of 1 [mass_unit/len_unit³] for mass properties of shapes.
> Consequently, the default mass of a unit cube is 1 [mass_unit].
> The default value for gravity is 9.81 [len_unit/time_unit²], which aligns with the gravitational acceleration on Earth in metric units (m/s²).
> Therefore, it is reasonable to use metric units (kg, m, s) when conceptualizing these values.
The smaller the time step size, the more stable the simulation.
Timesteps larger than $\mathrm{dt}=1/60\,\mathrm{s}$ are not advised.
It is also recommended to use fixed time steps.
Typical code accumulates delta times and calls `world.Step` only at fixed time intervals, as shown in the following example.
```cs
private float accumulatedTime = 0.0f;
public void FixedTimeStep(float dt, int maxSteps = 4)
{
const float fixedStep = 1.0f / 100.0f;
int steps = 0;
accumulatedTime += dt;
while (accumulatedTime > fixedStep)
{
world.Step(fixedStep, multiThread);
accumulatedTime -= fixedStep;
// we can not keep up with the real time, i.e. the simulation
// is running slower than the real time is passing.
if (++steps >= maxSteps) return;
}
}
```
### Multithreading
Jitter2 employs its own thread pool (`Parallelization.ThreadPool`) to distribute tasks across multiple threads.
The thread pool is utilized when `world.Step` is invoked with `multiThread` set to true.
By default, `ThreadPool.ThreadCountSuggestion`$-1$ additional threads are spawned, where the suggestion is calculated by
```cs
public const float ThreadsPerProcessor = 0.9f;
public static int ThreadCountSuggestion => Math.Max((int)(Environment.ProcessorCount * ThreadsPerProcessor), 1);
```
The number of worker threads managed by the thread pool can be adjusted using `ChangeThreadCount(int numThreads)`.
A singleton pattern is used here, as demonstrated below:
```cs
ThreadPool.Instance.ChangeThreadCount(4);
```
This adjusts the number of additional (with respect to the main thread) worker threads to $4-1=3$.
The `world.ThreadModel` property may be used to keep the thread pool in a tight loop waiting for work to be processed after `world.Step` has been run (`ThreadModelType.Persistent`), or to yield threads afterwards (`ThreadModelType.Regular`).
The latter option is recommended to free processing power for other code, such as rendering.
## Solver Mode
Jitter2 offers two solver strategies through :
```cs
world.SolveMode = SolveMode.Regular; // default
world.SolveMode = SolveMode.Deterministic; // reproducible, slower
```
`SolveMode.Regular` is the default and optimized for throughput. It keeps the solver fast, but the exact order in which contacts and constraints are processed may vary.
`SolveMode.Deterministic` is the best-effort cross-platform deterministic path. It processes each simulation island in a stable order and uses deterministic tie-breakers for contacts and constraints. Islands can still be distributed across threads, so `world.Step(dt, multiThread: true)` remains valid in deterministic mode.
Use deterministic mode when repeatability matters more than raw performance, for example for tests, replays, debugging, or deterministic gameplay simulation. It can be significantly slower than `SolveMode.Regular`, so it should generally be enabled deliberately rather than used as the default for all worlds.
By contrast, `SolveMode.Regular` with `multiThread: false` can still look reproducible when the world is built in the exact same way inside the same .NET process, but that is a much weaker property and should not be confused with cross-platform determinism.
For deterministic mode, it is not necessary that the entire world was built through the same history. What must match is the setup order inside each interacting island: the participating bodies, shapes, and constraints must be added in the same order. Precision still matters: a float build and a double build are both deterministic, but they will not produce the same bit pattern. See [General](general.md#deterministic-simulation) for a summary of what each solver configuration guarantees and the current CI coverage behind that claim.
## Solver Iterations
Jitter2 employs an iterative solver to solve contacts and constraints.
The number of iterations can be raised to improve simulation quality (`world.SolverIterations`).
```cs
world.SolverIterations = (solver: 6, relaxation: 4);
```
Jitter2 solves physical contacts (and constraints) on the velocity level ('solver iterations').
Jitter2 also adds velocities to rigid bodies to resolve unphysical interpenetrations of bodies.
These additional velocities add unwanted energy to the system which can be removed by an additional relaxation phase after integrating the new positions from these velocities.
The number of iterations in the relaxation phase ('relaxation iterations') is specified here as well.
The runtime for solving contacts and constraints scales linearly with the number of iterations.
## Substep Count
The time step can be divided into smaller steps, defined by `world.SubstepCount`.
These smaller time steps are solved similar to regular full steps, however collision information is not updated.
Each sub step is solved with the number of solver iterations specified in `world.SolverIterations`.
For example
```cs
world.SubstepCount = 4;
world.SolverIterations = (solver: 2, relaxation: 1);
```
does perform $12$ solver iterations in total for each call to `world.Step`.
The runtime is slower than a single regular step with $12$ iterations but this approach enhances the stability of the simulation.
Substepping is excellent for enhancing the overall quality of constraints, stabilizing large stacks of objects, and simulating large mass ratios (like heavy objects resting on light objects) with greater accuracy.
## Contact Manifold Persistence
By default, Jitter2 caches contact points and their accumulated impulses between frames (`world.PersistentContactManifold = true`). This allows the solver to warm-start from the previous solution and lets the manifold grow over several steps, which improves stability for resting contacts.
Setting `world.PersistentContactManifold = false` discards all contact data at the end of each frame, so every contact is treated as brand-new. This removes frame-to-frame contact memory at the cost of solver convergence speed.
Individual bodies can discard their cached contacts without changing the global setting:
```cs
body.ClearContactCache(); // discard cached manifold for this body
```
This is useful after discontinuous user-driven transforms such as teleports. When `SolveMode.Deterministic` is active, setting a body's `Position` or `Orientation` property automatically calls `ClearContactCache()`.
## Warm-Start Reset
The iterative solver warm-starts each frame from the accumulated impulses of the previous frame. After restoring a snapshot or any other discontinuous state change, this cached state can be stale. Every constraint exposes a `ResetWarmStart()` method that clears its accumulated impulses without removing the constraint:
```cs
foreach (var constraint in body.Constraints)
{
constraint.ResetWarmStart();
}
```
`World.Stabilize` can then be called to re-solve the restored contacts and constraints before resuming normal simulation. `Stabilize` respects `SolveMode.Deterministic` when it is set.
## Auxiliary Contacts
Jitter2 employs a technique termed 'auxiliary contacts', where additional contacts are generated for the general case where two flat surfaces of shapes are in contact.
These additional contacts are calculated within one frame, generating the full contact manifold in a 'single pass' and preventing jitter commonly encountered with incrementally constructed collision manifolds.
The `world.EnableAuxiliaryContactPoints` property can be used to enable or disable the usage of auxiliary contact point generation.
## Rigid Bodies
All rigid bodies registered with the world can be accessed using
```cs
world.RigidBodies
```
where `RigidBodies` is of type `ReadOnlyPartitionedSet`.
The bodies are in no particular order and may be reordered during calls to `world.Step`.
## Raw Data
`RigidBody`s, `Arbiter`s, and `(Small)Constraint`s are regular C# classes that reside on the managed heap.
However, these objects are linked to their unmanaged counterparts: `RigidBodyData`, `ContactData`, and `(Small)ConstraintData` which can be accessed using:
```cs
world.RawData
```
Jitter2 relocates native structures so that active objects are stored in contiguous memory, enabling efficient access by the iterative solver.
> [!CAUTION]
> **Raw Memory Access**
> Accessing raw memory is generally not required when utilizing the standard functionalities of Jitter2.
> Although reading the raw data of objects is generally safe, modifying data can corrupt the internal state of the engine.
> [!WARNING]
> **Accessing Removed Entities**
> Instances of `RigidBody`, `Arbiter`, and `Constraint` store some of their data in unmanaged memory, which is automatically freed once the entities are removed (`world.Remove`) from the world.
> These entities must not be used any longer, i.e., their functions and properties must not be called or accessed, otherwise, a `NullReferenceException` is thrown.
## Removing Entities
Rigid bodies, constraints, and shapes can be removed from the world:
```cs
world.Remove(body); // removes body and its shapes
world.Remove(constraint); // removes a constraint
world.Clear(); // removes all entities
```
## NullBody
The world provides a special static body `world.NullBody` that is pinned to the world.
It can be used to create constraints that fix a body relative to world space (see [Constraints](constraints.md)).
## Deactivation
The deactivation system can be globally disabled using `world.AllowDeactivation`.
Setting this to `false` prevents bodies from being deactivated but does not wake up already sleeping bodies.
================================================
FILE: docfx/docs/introduction.md
================================================
---
_disableBreadcrumb: true
_disableContribution: true
---
Fast · Lightweight · Pure C# · MIT License
Jitter Physics 2
An impulse-based 3D rigid-body physics engine for .NET —
zero native dependencies, runs anywhere .NET runs.
⚡
Zero Dependencies
Pure C# with no native DLLs, no P/Invoke. Runs on any platform that supports .NET 8 or later.
🔨
Impulse-Based Dynamics
Semi-implicit Euler integrator with sub-stepping for stable, stiff simulations at interactive frame rates.
🧩
Deterministic Solver
Optional cross-platform deterministic mode for reproducible simulations.
💤
Massive Sleeping Worlds
Deactivation system keeps large simulations fast — 100k+ inactive bodies have near-zero per-frame cost.
🎯
Speculative Contacts
Prevents tunneling at high velocities without the overhead of continuous collision detection.
🔗
Rich Constraints
BallSocket, Hinge, Prismatic, Universal Joint, Linear & Angular Motor, Spring, Distance Limit, and more.
💎
Convex & Mesh Shapes
Box, Capsule, Sphere, Cone, Cylinder, ConvexHull, PointCloud, and TriangleMesh — unified GJK/MPR pipeline.
🔄
Float or Double Precision
Compile-time precision switch — the same API and source code works with either float or double.
🧷
Soft-Body Dynamics
Cloth, ropes, and deformable bodies coexist alongside rigid bodies in the same simulation world.
Interactive Web Demo
Run the physics engine live in your browser — no install required.
================================================
FILE: docfx/docs/toc.yml
================================================
- name: Welcome
href: introduction.md
- name: Quickstart
expanded: true
items:
- name: Boxes
expanded: true
items:
- name: Project Setup
href: tutorials/boxes/project-setup.md
- name: Render Loop
href: tutorials/boxes/render-loop.md
- name: Hello World
href: tutorials/boxes/hello-world.md
- name: Teapots
expanded: true
items:
- name: Project Setup
href: tutorials/teapots/project-setup.md
- name: Hull Sampling
href: tutorials/teapots/hull-sampling.md
- name: Hello World
href: tutorials/teapots/hello-world.md
- name: Aftermath
href: tutorials/teapots/aftermath.md
- name: Overview
expanded: true
items:
- name: General
href: documentation/general.md
- name: World
href: documentation/world.md
- name: Rigid Bodies
href: documentation/bodies.md
- name: Shapes
href: documentation/shapes.md
- name: Arbiters
href: documentation/arbiters.md
- name: Constraints
href: documentation/constraints.md
- name: Dynamic Tree
href: documentation/dynamictree.md
- name: Collision Filters
href: documentation/filters.md
- name: Changelog
href: changelog.md
================================================
FILE: docfx/docs/tutorials/boxes/hello-world.md
================================================
# Hello World
We will now add physics to the scene. We do this by creating a new instance of the World class and adding several rigid bodies to it.
Replace the content of `Program.cs` with the following code (marked lines indicate the additions to the source code):
```cs
using System.Numerics;
using Raylib_cs;
using Jitter2;
using Jitter2.Collision.Shapes;
using Jitter2.Dynamics;
using Jitter2.LinearMath;
using static Raylib_cs.Raylib;
static Matrix4x4 GetRayLibTransformMatrix(RigidBody body)
{
JMatrix ori = JMatrix.CreateFromQuaternion(body.Orientation);
JVector pos = body.Position;
return new Matrix4x4(ori.M11, ori.M12, ori.M13, pos.X,
ori.M21, ori.M22, ori.M23, pos.Y,
ori.M31, ori.M32, ori.M33, pos.Z,
0, 0, 0, 1.0f);
}
static Texture2D GenCheckedTexture(int size, int checks, Color colorA, Color colorB)
{
Image imageMag = GenImageChecked(size, size, checks, checks, colorA, colorB);
Texture2D textureMag = LoadTextureFromImage(imageMag);
UnloadImage(imageMag);
return textureMag;
}
const int NumberOfBoxes = 12;
// set a hint for anti-aliasing
SetConfigFlags(ConfigFlags.Msaa4xHint);
// initialize a 1200x800 px window with a title
InitWindow(1200, 800, "BoxDrop example");
// dynamically create a plane model
Texture2D texture = GenCheckedTexture(10, 1, Color.LightGray, Color.Gray);
Model planeModel = LoadModelFromMesh(GenMeshPlane(10, 10, 10, 10));
SetMaterialTexture(ref planeModel, 0, MaterialMapIndex.Diffuse, ref texture);
// dynamically create a box model
texture = GenCheckedTexture(2, 1, Color.White, Color.Magenta);
Mesh boxMesh = GenMeshCube(1, 1, 1);
Material boxMat = LoadMaterialDefault();
SetMaterialTexture(ref boxMat, MaterialMapIndex.Diffuse, texture);
// initialize the Jitter physics world
World world = new ();
world.SubstepCount = 4;
// add a body representing the plane
RigidBody planeBody = world.CreateRigidBody();
planeBody.AddShape(new BoxShape(10));
planeBody.Position = new JVector(0, -5, 0);
planeBody.MotionType = MotionType.Static;
// add NumberOfBoxes cubes
for(int i = 0; i < NumberOfBoxes; i++)
{
RigidBody body = world.CreateRigidBody();
body.AddShape(new BoxShape(1));
body.Position = new JVector(0, i * 2 + 0.5f, 0);
}
// create a camera
Camera3D camera = new ()
{
Position = new Vector3(-20.0f, 8.0f, 10.0f),
Target = new Vector3(0.0f, 4.0f, 0.0f),
Up = new Vector3(0.0f, 1.0f, 0.0f),
FovY = 45.0f,
Projection = CameraProjection.Perspective
};
// 100 fps target
SetTargetFPS(100);
// simple render loop
while (!WindowShouldClose())
{
BeginDrawing();
ClearBackground(Color.Blue);
BeginMode3D(camera);
DrawModel(planeModel, Vector3.Zero, 1.0f, Color.White);
world.Step(1.0f / 100.0f, true);
foreach(var body in world.RigidBodies)
{
if (body == planeBody || body == world.NullBody) continue; // do not draw this
DrawMesh(boxMesh, boxMat, GetRayLibTransformMatrix(body));
}
EndMode3D();
DrawText($"{GetFPS()} fps", 10, 10, 20, Color.White);
EndDrawing();
}
CloseWindow();
```
Running your program, you should now see a few boxes dynamically falling onto the ground.

================================================
FILE: docfx/docs/tutorials/boxes/project-setup.md
================================================
# Project Setup
In this project we will use Raylib and Jitter to implement a simple scene of boxes falling to the ground.
## Requirements
Install the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0).
Ensure that dotnet is correctly set up by executing the following command:
```sh
dotnet --version
```
## Create a New Console Application and Add Jitter and Raylib
First, create a new directory named "BoxDrop" and navigate into it:
```sh
mkdir BoxDrop && cd BoxDrop
```
Next, create a new console application in this directory and add Raylib-cs and Jitter2:
```sh
dotnet new console
dotnet add package Raylib-cs --version 6.1.1
dotnet add package Jitter2
```
You have completed the setup. If you now execute the following command:
```sh
dotnet run
```
Your console should display: "Hello, World!".
================================================
FILE: docfx/docs/tutorials/boxes/render-loop.md
================================================
# Render Loop
The first thing we need to do is to familiarize ourselves a bit with Raylib_cs. Replace the content of `Program.cs` with the following code:
```cs
using System.Numerics;
using Raylib_cs;
using static Raylib_cs.Raylib;
static Texture2D GenCheckedTexture(int size, int checks, Color colorA, Color colorB)
{
Image imageMag = GenImageChecked(size, size, checks, checks, colorA, colorB);
Texture2D textureMag = LoadTextureFromImage(imageMag);
UnloadImage(imageMag);
return textureMag;
}
// set a hint for anti-aliasing
SetConfigFlags(ConfigFlags.Msaa4xHint);
// initialize a 1200x800 px window with a title
InitWindow(1200, 800, "BoxDrop example");
// dynamically create a plane model
Texture2D texture = GenCheckedTexture(10, 1, Color.LightGray, Color.Gray);
Model planeModel = LoadModelFromMesh(GenMeshPlane(10, 10, 10, 10));
SetMaterialTexture(ref planeModel, 0, MaterialMapIndex.Diffuse, ref texture);
// create a camera
Camera3D camera = new ()
{
Position = new Vector3(-20.0f, 8.0f, 10.0f),
Target = new Vector3(0.0f, 4.0f, 0.0f),
Up = new Vector3(0.0f, 1.0f, 0.0f),
FovY = 45.0f,
Projection = CameraProjection.Perspective
};
// 100 fps target
SetTargetFPS(100);
// simple render loop
while (!WindowShouldClose())
{
BeginDrawing();
ClearBackground(Color.Blue);
BeginMode3D(camera);
DrawModel(planeModel, Vector3.Zero, 1.0f, Color.White);
EndMode3D();
DrawText($"{GetFPS()} fps", 10, 10, 20, Color.White);
EndDrawing();
}
CloseWindow();
```
Running your program should now display a plane:

We will add some physically simulated boxes in the next chapter.
================================================
FILE: docfx/docs/tutorials/teapots/aftermath.md
================================================
# Aftermath
### `PointCloudShape` vs `ConvexHullShape`
In this example, we used the `PointCloudShape` to simulate a rigid body with a convex hull. As the name suggests, this shape only requires a set of points — they don't need to lie exactly on a convex surface. By design, Jitter treats the convex hull of these points as the actual shape for collision detection.
Although the algorithm used here is brute-force, it can be extremely fast: the data is stored in a linear memory layout, and SIMD instructions are used to accelerate the support function. Because of this, `PointCloudShape` is usually the best choice for quickly and efficiently adding simple convex geometry to your simulation.
The `ConvexHullShape`, on the other hand, is intended for more complex and detailed convex models. Unlike `PointCloudShape`, this shape requires a precomputed convex hull provided as a list of triangles (`List`). Jitter does not generate this for you — you'll need to use third-party tools like Blender or dedicated convex hull libraries. The input mesh must be strictly convex for collision detection to work correctly.
Internally, `ConvexHullShape` uses a hill-climbing algorithm to compute the support function. While this approach is algorithmically more efficient than brute-force, the performance benefits only become noticeable with larger shapes. As a general rule of thumb, `ConvexHullShape` starts to outperform `PointCloudShape` at around 300 vertices or more.
================================================
FILE: docfx/docs/tutorials/teapots/hello-world.md
================================================
# Hello World
### Creating the `PointCloudShape`
We can now create a `PointCloudShape` from the sampled vertices:
```cs
// find a few points on the convex hull of the teapot.
var vertices = ShapeHelper.SampleHull(allVertices, subdivisions: 3);
// use these points to create a PointCloudShape.
var pointCloudShape = new PointCloudShape(vertices);
```
However, we need to be a bit careful here.
If we add this shape to a rigid body as-is, the body may not behave as intuitively expected.
This is because the center of mass of a rigid body is always located at `(0, 0, 0)` in its local coordinate frame.
If you open `teapot.obj` in a model editor, you'll notice that the model is not centered around the origin.
To correct this, we either need to center the model manually in a model editor—or, more conveniently, use the `Shift` property of `PointCloudShape` to align the center of mass with the origin:
```cs
// find a few points on the convex hull of the teapot.
var vertices = ShapeHelper.SampleHull(allVertices, subdivisions: 3);
// use these points to create a PointCloudShape.
var pointCloudShape = new PointCloudShape(vertices);
// shift the shape so its center of mass is at the origin.
pointCloudShape.GetCenter(out JVector centerOfMass);
pointCloudShape.Shift = -centerOfMass;
// pointCloudShape.GetCenter(out centerOfMass); // now returns (0, 0, 0)
// finally, create the rigid body for the teapot
var rigidBody = world.CreateRigidBody();
rigidBody.AddShape(pointCloudShape);
```
> [!WARNING]
> The shift applied to the shape must also be taken into account when rendering the model, to ensure it aligns visually with the simulation.
#### Creating Multiple Instances of the Same Shape
In Jitter, it is not valid to add the same shape instance to multiple rigid bodies.
To create additional instances of a shape, use the `Clone()` method of `PointCloudShape`.
This method creates a new shape object that shares the underlying data structure, saving both memory and computation time:
```cs
var shapeInstance1 = new PointCloudShape(vertices);
var shapeInstance2 = shapeInstance1.Clone(); // Safe to use in a second body
```
This approach is especially useful when many bodies share the same geometry, such as multiple identical props or characters in a simulation.
### Putting it all together
```cs
using System.Numerics;
using Raylib_cs;
using Jitter2;
using Jitter2.Collision.Shapes;
using Jitter2.Dynamics;
using Jitter2.LinearMath;
using static Raylib_cs.Raylib;
static Matrix4x4 GetRayLibTransformMatrix(RigidBody body)
{
JMatrix ori = JMatrix.CreateFromQuaternion(body.Orientation);
JVector pos = body.Position;
return new Matrix4x4(ori.M11, ori.M12, ori.M13, pos.X,
ori.M21, ori.M22, ori.M23, pos.Y,
ori.M31, ori.M32, ori.M33, pos.Z,
0, 0, 0, 1.0f);
}
static Texture2D GenCheckedTexture(int size, int checks, Color colorA, Color colorB)
{
Image imageMag = GenImageChecked(size, size, checks, checks, colorA, colorB);
Texture2D textureMag = LoadTextureFromImage(imageMag);
UnloadImage(imageMag);
return textureMag;
}
const int numberOfTeapots = 12;
// set a hint for anti-aliasing
SetConfigFlags(ConfigFlags.Msaa4xHint);
// initialize a 1200x800 px window with a title
InitWindow(1200, 800, "TeaDrop example");
// dynamically create a plane model
Texture2D texture = GenCheckedTexture(10, 1, Color.LightGray, Color.Gray);
Model planeModel = LoadModelFromMesh(GenMeshPlane(20, 20, 10, 10));
SetMaterialTexture(ref planeModel, 0, MaterialMapIndex.Diffuse, ref texture);
// load the teapot model from file
Model teapotModel = LoadModel("teapot.obj");
// load the mesh vertices
if (teapotModel.MeshCount == 0)
throw new Exception("Model could not be loaded!");
Mesh teapotMesh;
unsafe { teapotMesh = teapotModel.Meshes[0]; }
var allVertices = teapotMesh.VerticesAs();
// sample vertices on the convex hull
var vertices = ShapeHelper.SampleHull(allVertices, 4);
// create the PointCloudShape from the reduced vertices
var pointCloudShape = new PointCloudShape(vertices);
// shift the shape, such that the center of mass is at the origin
pointCloudShape.GetCenter(out JVector center);
pointCloudShape.Shift = -center;
// we need to take the transpose here, since Raylib and System.Numerics
// use a different convention
Matrix4x4 shift = Matrix4x4.CreateTranslation(-center);
shift = Matrix4x4.Transpose(shift);
texture = GenCheckedTexture(16, 2, Color.White, Color.Magenta);
Material teapotMat = LoadMaterialDefault();
SetMaterialTexture(ref teapotMat, MaterialMapIndex.Diffuse, texture);
// initialize the Jitter physics world
World world = new ();
world.SubstepCount = 4;
// add a body representing the plane
RigidBody planeBody = world.CreateRigidBody();
planeBody.AddShape(new BoxShape(20));
planeBody.Position = new JVector(0, -10, 0);
planeBody.MotionType = MotionType.Static;
// add numberOfTeapots teapots
for(int i = 0; i < numberOfTeapots; i++)
{
RigidBody body = world.CreateRigidBody();
body.AddShape(pointCloudShape.Clone());
body.Position = new JVector(0, i * 4 + 0.5f, 0);
}
// create a camera
Camera3D camera = new ()
{
Position = new Vector3(-40.0f, 16.0f, 20.0f),
Target = new Vector3(0.0f, 4.0f, 0.0f),
Up = new Vector3(0.0f, 1.0f, 0.0f),
FovY = 45.0f,
Projection = CameraProjection.Perspective
};
// 100 fps target
SetTargetFPS(100);
// simple render loop
while (!WindowShouldClose())
{
BeginDrawing();
ClearBackground(Color.Blue);
BeginMode3D(camera);
DrawModel(planeModel, Vector3.Zero, 1.0f, Color.White);
world.Step(1.0f / 100.0f, true);
foreach(var body in world.RigidBodies)
{
if (body == planeBody || body == world.NullBody) continue; // do not draw this
DrawMesh(teapotMesh, teapotMat, GetRayLibTransformMatrix(body) * shift);
}
EndMode3D();
DrawText($"{GetFPS()} fps", 10, 10, 20, Color.White);
EndDrawing();
}
CloseWindow();
```

================================================
FILE: docfx/docs/tutorials/teapots/hull-sampling.md
================================================
# Hull Sampling
The teapot is a concave shape, which we will approximate using its convex hull ([Wikipedia](https://en.wikipedia.org/wiki/Convex_hull)).
As a first step, we will reduce the number of vertices used to construct the convex hull.
> [!NOTE]
> **Sampling**
>
> We could work with the entire set of vertices, but this comes with two main disadvantages:
>
> 1. The set may include vertices that lie inside the hull. While these do not affect the simulation outcome, they are still briefly considered during collision detection in each frame.
>
> 2. We typically don't need an exact convex hull. Omitting a few vertices often has little effect on accuracy while significantly improving performance.
>
> Reducing the number of vertices can help speed up the simulation without sacrificing collision fidelity.
Jitter provides a built-in method to simplify the vertex set: `ShapeHelper.SampleHull`.
This function samples directions uniformly across the unit sphere and returns the vertices that are furthest away in those directions. These vertices, by definition, lie on the convex hull of the original shape.
Replace the content of `Program.cs` with the following code:
```cs
using System.Numerics;
using Jitter2.Collision.Shapes;
using Jitter2.LinearMath;
using Raylib_cs;
using static Raylib_cs.Raylib;
// set a hint for anti-aliasing
SetConfigFlags(ConfigFlags.Msaa4xHint);
// initialize a 1200x800 px window with a title
InitWindow(1200, 800, "TeaDrop example");
// load the teapot model from a file
Model teapotModel = LoadModel("teapot.obj");
// load the mesh vertices
if (teapotModel.MeshCount == 0)
throw new Exception("Model could not be loaded!");
Mesh mesh;
unsafe { mesh = teapotModel.Meshes[0]; }
var allVertices = mesh.VerticesAs();
var vertices = ShapeHelper.SampleHull(allVertices, 4);
// just for visualization -
// we will not need to construct the explicit hull for the simulation!
var debugHull = ShapeHelper.Tessellate(vertices, 4);
// create a camera
Camera3D camera = new ()
{
Position = new Vector3(-5.0f, 2.0f, 3.0f),
Target = new Vector3(0.0f, 1.0f, 0.0f),
Up = new Vector3(0.0f, 1.0f, 0.0f),
FovY = 45.0f,
Projection = CameraProjection.Perspective
};
// 100 fps target
SetTargetFPS(100);
// simple render loop
while (!WindowShouldClose())
{
BeginDrawing();
ClearBackground(Color.Blue);
// make the scene rotate
UpdateCamera(ref camera, CameraMode.Orbital);
BeginMode3D(camera);
// draw the teapot model
DrawModelWires(teapotModel, Vector3.Zero, 1.0f, Color.White);
// draw the sample vertices of the convex hull
foreach (var vertex in vertices)
{
DrawSphere(vertex, 0.02f, Color.Red);
}
// for debugging: draw the explicit hull
foreach (var vertex in debugHull)
{
DrawLine3D(vertex.V0, vertex.V1, Color.Green);
DrawLine3D(vertex.V1, vertex.V2, Color.Green);
DrawLine3D(vertex.V2, vertex.V0, Color.Green);
}
EndMode3D();
DrawText($"{GetFPS()} fps", 10, 10, 20, Color.White);
EndDrawing();
}
CloseWindow();
```

The teapot's wireframe is drawn in white, the green wireframe shows the convex hull, and the sampled hull vertices are depicted as red dots.
================================================
FILE: docfx/docs/tutorials/teapots/project-setup.md
================================================
# Project Setup
In the previous section, we created a simulation of falling boxes. Jitter includes several default shapes, such as capsules, cylinders, and spheres.
These shapes can be transformed and/or combined, and they are already sufficient to represent many types of collidable entities.
In this section, we will add a custom convex shape to the simulation—specifically, the famous *Utah teapot*. We'll construct this shape from its visual representation by loading a `teapot.obj` file and using its vertices to create the convex shape.
### Requirements
Install the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0).
Ensure that dotnet is correctly set up by executing the following command:
```sh
dotnet --version
```
### Create a New Console Application and Add Jitter and Raylib
First, create a new directory named "TeaDrop" and navigate into it:
```sh
mkdir TeaDrop && cd TeaDrop
```
Download and unzip the [teapot.obj](https://github.com/notgiven688/jitterphysics2/raw/refs/heads/main/src/JitterDemo/assets/teapot.obj.zip) model.
```sh
wget https://github.com/notgiven688/jitterphysics2/raw/refs/heads/main/src/JitterDemo/assets/teapot.obj.zip
unzip teapot.obj.zip
```
Next, create a new console application in this directory and add Raylib-cs and Jitter2:
```sh
dotnet new console
dotnet add package Raylib-cs --version 6.1.1
dotnet add package Jitter2
```
Add the following code to `TeaDrop.csproj` to allow unsafe code, and to copy teapot.obj automatically to the output directory:
```xml
true
Always
```
You have completed the setup. If you now execute the following command:
```sh
dotnet run
```
Your console should display: "Hello, World!".
================================================
FILE: docfx/index.md
================================================
---
_layout: redirect
redirect_url: docs/introduction.html
---
================================================
FILE: docfx/run.sh
================================================
#!/bin/bash
docfx metadata
docfx build
docfx serve _site
================================================
FILE: docfx/template/public/main.js
================================================
export default {
iconLinks: [
{
icon: 'github',
href: 'https://github.com/notgiven688/jitterphysics2',
title: 'GitHub'
},
{
icon: 'box-seam',
href: 'https://www.nuget.org/packages/Jitter2',
title: 'NuGet'
}
]
}
================================================
FILE: docfx/toc.yml
================================================
- name: Docs
href: ./docs/
- name: API (single precision)
href: api/
================================================
FILE: other/ContactClipping/ClipDebug.cs
================================================
using System;
using System.Collections.Generic;
using System.Numerics;
namespace JitterClipVisualizer;
internal readonly record struct ClipPoint(float X, float Y)
{
public static ClipPoint operator +(ClipPoint left, ClipPoint right)
{
return new ClipPoint(left.X + right.X, left.Y + right.Y);
}
public static ClipPoint operator -(ClipPoint left, ClipPoint right)
{
return new ClipPoint(left.X - right.X, left.Y - right.Y);
}
public static ClipPoint operator *(float scale, ClipPoint point)
{
return new ClipPoint(scale * point.X, scale * point.Y);
}
public float LengthSquared()
{
return X * X + Y * Y;
}
public Vector2 ToVector2()
{
return new Vector2(X, Y);
}
}
internal enum ClipDebugMode
{
PolygonClip,
SegmentAgainstPolygon,
SegmentAgainstSegment,
NoIntersection
}
internal sealed record ClipStep(
string Title,
string Summary,
ClipPoint[] InputPolygon,
ClipPoint[] OutputPolygon,
int EdgeIndex = -1,
ClipPoint EdgeStart = default,
ClipPoint EdgeEnd = default);
internal sealed record ClipSnapshot(
string PresetName,
string Description,
ClipDebugMode Mode,
string Status,
ClipPoint[] Left,
ClipPoint[] Right,
ClipPoint[] Result,
ClipStep[] Steps);
internal sealed record ClipFrame(ClipPoint[] Left, ClipPoint[] Right);
internal sealed record ClipPreset(string Name, string Description, Func BuildFrame);
internal static class ContactManifoldClipDebugger
{
private const int MaxManifoldPoints = 6;
private const int MaxClipPoints = 12;
private const int FinalVertexLimit = 4;
private static readonly ClipPoint[] polygonA =
[
new ClipPoint(100.0f, 0.0f),
new ClipPoint(50.0f, 86.0f),
new ClipPoint(-50.0f, 86.0f),
new ClipPoint(-100.0f, 0.0f),
new ClipPoint(-50.0f, -86.0f),
new ClipPoint(50.0f, -86.0f)
];
private static readonly ClipPoint[] polygonB =
[
new ClipPoint(118.0f, 0.0f),
new ClipPoint(36.0f, 110.0f),
new ClipPoint(-96.0f, 68.0f),
new ClipPoint(-96.0f, -68.0f),
new ClipPoint(36.0f, -110.0f)
];
private static readonly ClipPoint[] lineA =
[
new ClipPoint(-165.0f, 0.0f),
new ClipPoint(165.0f, 0.0f)
];
private static readonly ClipPoint[] lineB =
[
new ClipPoint(-130.0f, 0.0f),
new ClipPoint(130.0f, 0.0f)
];
public static IReadOnlyList CreatePresets()
{
return
[
new ClipPreset(
"Polygon Clip",
"The Sutherland-Hodgman style pass sequence used for area overlap in CollisionManifold.",
BuildPolygonClipFrame),
new ClipPreset(
"Segment vs Polygon",
"The linear fallback path used when one projected feature collapses to a segment.",
BuildSegmentVsPolygonFrame),
new ClipPreset(
"Segment vs Segment",
"The segment-overlap helper used when both projected features collapse to lines.",
BuildSegmentVsSegmentFrame)
];
}
public static ClipSnapshot BuildSnapshot(ClipPreset preset, float time)
{
ClipFrame frame = preset.BuildFrame(time);
return BuildSnapshot(preset.Name, preset.Description, frame.Left, frame.Right);
}
public static ClipSnapshot BuildSnapshot(string presetName, string description, ClipPoint[] leftInput, ClipPoint[] rightInput)
{
if (leftInput.Length == 0 || rightInput.Length == 0)
{
return new ClipSnapshot(
presetName,
description,
ClipDebugMode.NoIntersection,
"Both inputs need at least one point.",
leftInput,
rightInput,
[],
[
new ClipStep(
"Invalid input",
"At least one polygon was empty.",
Copy(leftInput, leftInput.Length),
[])
]);
}
if (leftInput.Length > MaxClipPoints || rightInput.Length > MaxClipPoints)
{
throw new ArgumentOutOfRangeException(nameof(leftInput),
$"This visualizer mirrors the manifold clipper limits and supports up to {MaxClipPoints} points per feature.");
}
ClipPoint[] left = new ClipPoint[MaxClipPoints];
ClipPoint[] right = new ClipPoint[MaxClipPoints];
Array.Copy(leftInput, left, leftInput.Length);
Array.Copy(rightInput, right, rightInput.Length);
int leftCount = leftInput.Length;
int rightCount = rightInput.Length;
CalculateClipTolerance(left, leftCount, right, rightCount,
out float sideEpsilon, out float distanceEpsilonSq, out float areaEpsilon);
CompactPolygon(left, ref leftCount, distanceEpsilonSq, areaEpsilon);
CompactPolygon(right, ref rightCount, distanceEpsilonSq, areaEpsilon);
if (leftCount > 2) NormalizeWinding(left, leftCount);
if (rightCount > 2) NormalizeWinding(right, rightCount);
List steps =
[
new ClipStep(
"Projected features",
$"Compacted to {leftCount} and {rightCount} points before clipping.",
Copy(left, leftCount),
Copy(left, leftCount))
];
ClipPoint[] clipped = new ClipPoint[MaxClipPoints];
int clippedCount = 0;
ClipDebugMode mode = ClipDebugMode.NoIntersection;
if (leftCount > 2 && rightCount > 2)
{
ClipPoint[] buffer = new ClipPoint[MaxClipPoints];
Array.Copy(left, clipped, leftCount);
clippedCount = ClipConvexPolygon(clipped, leftCount, right, rightCount, buffer,
sideEpsilon, distanceEpsilonSq, areaEpsilon, steps);
if (clippedCount > 0)
{
mode = ClipDebugMode.PolygonClip;
}
}
if (clippedCount == 0)
{
if (TryClipLinearIntersection(left, leftCount, right, rightCount,
sideEpsilon, distanceEpsilonSq, areaEpsilon, clipped, out clippedCount))
{
mode = leftCount == 2 && rightCount == 2
? ClipDebugMode.SegmentAgainstSegment
: ClipDebugMode.SegmentAgainstPolygon;
steps.Add(new ClipStep(
"Linear fallback",
$"Polygon clipping produced no area overlap, so the 1D fallback returned {clippedCount} point(s).",
Copy(left, leftCount),
Copy(clipped, clippedCount)));
}
else
{
steps.Add(new ClipStep(
"No overlap",
"Neither the polygon clipper nor the linear fallback found an intersection.",
Copy(left, leftCount),
[]));
}
}
CompactPolygon(clipped, ref clippedCount, distanceEpsilonSq, areaEpsilon);
ReducePolygon(clipped, ref clippedCount);
ClipPoint[] result = Copy(clipped, clippedCount);
steps.Add(new ClipStep(
"Final output",
clippedCount == 0
? "The final clipped feature is empty."
: $"The final clipped feature contains {clippedCount} point(s).",
Copy(left, leftCount),
result));
string status = mode switch
{
ClipDebugMode.PolygonClip => $"{clippedCount} point(s) from polygon-vs-polygon clipping.",
ClipDebugMode.SegmentAgainstPolygon => $"{clippedCount} point(s) from segment-against-polygon fallback.",
ClipDebugMode.SegmentAgainstSegment => $"{clippedCount} point(s) from segment-against-segment fallback.",
_ when clippedCount == 0 => "No overlap after both the polygon and linear clipping paths.",
_ => $"{clippedCount} point(s) returned."
};
return new ClipSnapshot(
presetName,
description,
mode,
status,
Copy(left, leftCount),
Copy(right, rightCount),
result,
steps.ToArray());
}
public static string GetModeLabel(ClipDebugMode mode)
{
return mode switch
{
ClipDebugMode.PolygonClip => "Polygon clip",
ClipDebugMode.SegmentAgainstPolygon => "Segment vs polygon fallback",
ClipDebugMode.SegmentAgainstSegment => "Segment vs segment fallback",
_ => "No overlap"
};
}
private static ClipFrame BuildPolygonClipFrame(float time)
{
Vector2 drift = new(
32.0f * MathF.Sin(time * 0.85f),
26.0f * MathF.Cos(time * 0.55f));
ClipPoint[] left = Transform(polygonA, drift, 0.45f + time * 0.55f, new Vector2(1.20f, 0.82f));
ClipPoint[] right = Transform(polygonB, new Vector2(12.0f, -10.0f), -0.42f, new Vector2(1.00f, 0.96f));
return new ClipFrame(left, right);
}
private static ClipFrame BuildSegmentVsPolygonFrame(float time)
{
Vector2 drift = new(
48.0f * MathF.Sin(time * 1.15f),
72.0f * MathF.Sin(time * 0.63f));
ClipPoint[] left = Transform(lineA, drift, -0.22f + 0.35f * MathF.Sin(time * 0.7f), Vector2.One);
ClipPoint[] right = Transform(polygonB, Vector2.Zero, -0.36f, new Vector2(0.95f, 0.78f));
return new ClipFrame(left, right);
}
private static ClipFrame BuildSegmentVsSegmentFrame(float time)
{
Vector2 leftOffset = new(
35.0f * MathF.Sin(time * 0.9f),
25.0f * MathF.Sin(time * 0.5f));
Vector2 rightOffset = new(
85.0f * MathF.Sin(time * 0.55f),
25.0f * MathF.Sin(time * 0.5f));
ClipPoint[] left = Transform(lineA, leftOffset, 0.0f, Vector2.One);
ClipPoint[] right = Transform(lineB, rightOffset, 0.0f, Vector2.One);
return new ClipFrame(left, right);
}
private static ClipPoint[] Transform(ClipPoint[] points, Vector2 translation, float rotation, Vector2 scale)
{
ClipPoint[] transformed = new ClipPoint[points.Length];
float cos = MathF.Cos(rotation);
float sin = MathF.Sin(rotation);
for (int i = 0; i < points.Length; i++)
{
float x = points[i].X * scale.X;
float y = points[i].Y * scale.Y;
transformed[i] = new ClipPoint(
x * cos - y * sin + translation.X,
x * sin + y * cos + translation.Y);
}
return transformed;
}
private static ClipPoint[] Copy(ClipPoint[] source, int count)
{
ClipPoint[] copy = new ClipPoint[count];
Array.Copy(source, copy, count);
return copy;
}
private static float Cross2D(in ClipPoint left, in ClipPoint right)
{
return left.X * right.Y - left.Y * right.X;
}
private static float SignedArea(ClipPoint[] polygon, int count)
{
if (count < 3) return 0.0f;
float area = 0.0f;
for (int i = 0; i < count; i++)
{
ClipPoint current = polygon[i];
ClipPoint next = polygon[(i + 1) % count];
area += Cross2D(current, next);
}
return area;
}
private static void ReversePolygon(ClipPoint[] polygon, int count)
{
for (int i = 0, j = count - 1; i < j; i++, j--)
{
(polygon[i], polygon[j]) = (polygon[j], polygon[i]);
}
}
private static void NormalizeWinding(ClipPoint[] polygon, int count)
{
if (SignedArea(polygon, count) < 0.0f)
{
ReversePolygon(polygon, count);
}
}
private static void CalculateClipTolerance(ClipPoint[] left, int leftCount,
ClipPoint[] right, int rightCount,
out float sideEpsilon, out float distanceEpsilonSq, out float areaEpsilon)
{
float scale = 1.0f;
for (int i = 0; i < leftCount; i++)
{
scale = MathF.Max(scale, MathF.Max(MathF.Abs(left[i].X), MathF.Abs(left[i].Y)));
}
for (int i = 0; i < rightCount; i++)
{
scale = MathF.Max(scale, MathF.Max(MathF.Abs(right[i].X), MathF.Abs(right[i].Y)));
}
float distanceEpsilon = 1e-5f * scale + 1e-7f;
distanceEpsilonSq = distanceEpsilon * distanceEpsilon;
areaEpsilon = distanceEpsilon * scale;
sideEpsilon = areaEpsilon;
}
private static void CompactPolygon(ClipPoint[] polygon, ref int count, float distanceEpsilonSq, float areaEpsilon)
{
if (count == 0) return;
int write = 0;
for (int i = 0; i < count; i++)
{
ClipPoint current = polygon[i];
if (write > 0 && (polygon[write - 1] - current).LengthSquared() <= distanceEpsilonSq)
{
continue;
}
polygon[write++] = current;
}
if (write > 1 && (polygon[0] - polygon[write - 1]).LengthSquared() <= distanceEpsilonSq)
{
write -= 1;
}
count = write;
if (count < 3) return;
bool removed;
do
{
removed = false;
for (int i = 0; i < count; i++)
{
ClipPoint previous = polygon[(i + count - 1) % count];
ClipPoint current = polygon[i];
ClipPoint next = polygon[(i + 1) % count];
ClipPoint edge0 = current - previous;
ClipPoint edge1 = next - current;
if (MathF.Abs(Cross2D(edge0, edge1)) > areaEpsilon) continue;
for (int j = i; j < count - 1; j++)
{
polygon[j] = polygon[j + 1];
}
count -= 1;
removed = true;
break;
}
}
while (removed && count >= 3);
}
private static float SideOfEdge(in ClipPoint edgeStart, in ClipPoint edgeEnd, in ClipPoint point)
{
return Cross2D(edgeEnd - edgeStart, point - edgeStart);
}
private static ClipPoint IntersectSegmentsAgainstEdge(in ClipPoint edgeStart, in ClipPoint edgeEnd,
in ClipPoint start, in ClipPoint end, float startSide, float endSide, float distanceEpsilonSq)
{
float denominator = startSide - endSide;
if (MathF.Abs(denominator) <= distanceEpsilonSq)
{
return MathF.Abs(startSide) <= MathF.Abs(endSide) ? start : end;
}
float t = startSide / denominator;
t = Math.Clamp(t, 0.0f, 1.0f);
return start + t * (end - start);
}
private static int ClipConvexPolygon(ClipPoint[] subject, int subjectCount,
ClipPoint[] clip, int clipCount, ClipPoint[] buffer,
float sideEpsilon, float distanceEpsilonSq, float areaEpsilon,
List steps)
{
ClipPoint[] input = subject;
ClipPoint[] output = buffer;
int inputCount = subjectCount;
bool resultInSubject = true;
for (int edge = 0; edge < clipCount; edge++)
{
if (inputCount == 0) return 0;
ClipPoint[] stepInput = Copy(input, inputCount);
ClipPoint edgeStart = clip[edge];
ClipPoint edgeEnd = clip[(edge + 1) % clipCount];
int outputCount = 0;
ClipPoint start = input[inputCount - 1];
float startSide = SideOfEdge(edgeStart, edgeEnd, start);
bool startInside = startSide >= -sideEpsilon;
for (int i = 0; i < inputCount; i++)
{
ClipPoint end = input[i];
float endSide = SideOfEdge(edgeStart, edgeEnd, end);
bool endInside = endSide >= -sideEpsilon;
if (startInside != endInside)
{
output[outputCount++] = IntersectSegmentsAgainstEdge(
edgeStart, edgeEnd, start, end, startSide, endSide, distanceEpsilonSq);
}
if (endInside)
{
output[outputCount++] = end;
}
start = end;
startSide = endSide;
startInside = endInside;
}
CompactPolygon(output, ref outputCount, distanceEpsilonSq, areaEpsilon);
steps.Add(new ClipStep(
$"Clip edge {edge + 1}/{clipCount}",
outputCount == 0
? "This edge rejected the current polygon completely."
: $"This pass produced {outputCount} output point(s).",
stepInput,
Copy(output, outputCount),
edge,
edgeStart,
edgeEnd));
ClipPoint[] temporary = input;
input = output;
output = temporary;
inputCount = outputCount;
resultInSubject = !resultInSubject;
}
if (!resultInSubject)
{
Array.Copy(input, subject, inputCount);
}
return inputCount;
}
private static void StoreLinearIntersection(in ClipPoint start, in ClipPoint end,
float distanceEpsilonSq, ClipPoint[] clipped, out int clippedCount)
{
clipped[0] = start;
clippedCount = 1;
if ((end - start).LengthSquared() <= distanceEpsilonSq) return;
clipped[1] = end;
clippedCount = 2;
}
private static bool ClipSegmentAgainstPolygon(in ClipPoint segmentStart, in ClipPoint segmentEnd,
ClipPoint[] polygon, int polygonCount,
float sideEpsilon, float distanceEpsilonSq, ClipPoint[] clipped, out int clippedCount)
{
float enter = 0.0f;
float exit = 1.0f;
ClipPoint delta = segmentEnd - segmentStart;
for (int edge = 0; edge < polygonCount; edge++)
{
ClipPoint edgeStart = polygon[edge];
ClipPoint edgeEnd = polygon[(edge + 1) % polygonCount];
float startSide = SideOfEdge(edgeStart, edgeEnd, segmentStart) + sideEpsilon;
float endSide = SideOfEdge(edgeStart, edgeEnd, segmentEnd) + sideEpsilon;
bool startInside = startSide >= 0.0f;
bool endInside = endSide >= 0.0f;
if (!startInside && !endInside)
{
clippedCount = 0;
return false;
}
if (startInside && endInside) continue;
float denominator = startSide - endSide;
if (MathF.Abs(denominator) <= distanceEpsilonSq)
{
clippedCount = 0;
return false;
}
float t = startSide / denominator;
t = Math.Clamp(t, 0.0f, 1.0f);
if (!startInside)
{
enter = MathF.Max(enter, t);
}
else
{
exit = MathF.Min(exit, t);
}
if (exit < enter)
{
clippedCount = 0;
return false;
}
}
StoreLinearIntersection(segmentStart + enter * delta, segmentStart + exit * delta,
distanceEpsilonSq, clipped, out clippedCount);
return true;
}
private static bool IntersectSegments(in ClipPoint leftStart, in ClipPoint leftEnd,
in ClipPoint rightStart, in ClipPoint rightEnd,
float sideEpsilon, float distanceEpsilonSq, float areaEpsilon,
ClipPoint[] clipped, out int clippedCount)
{
ClipPoint leftDelta = leftEnd - leftStart;
ClipPoint rightDelta = rightEnd - rightStart;
ClipPoint offset = rightStart - leftStart;
float cross = Cross2D(leftDelta, rightDelta);
const float parameterEpsilon = 1e-5f;
if (MathF.Abs(cross) <= areaEpsilon)
{
if (MathF.Abs(Cross2D(offset, leftDelta)) > areaEpsilon)
{
clippedCount = 0;
return false;
}
bool useLeft = leftDelta.LengthSquared() >= rightDelta.LengthSquared();
ClipPoint baseStart = useLeft ? leftStart : rightStart;
ClipPoint baseDelta = useLeft ? leftDelta : rightDelta;
bool useXAxis = MathF.Abs(baseDelta.X) >= MathF.Abs(baseDelta.Y);
float baseOrigin = useXAxis ? baseStart.X : baseStart.Y;
float baseExtent = useXAxis ? baseDelta.X : baseDelta.Y;
if (MathF.Abs(baseExtent) <= sideEpsilon)
{
clippedCount = 0;
return false;
}
float leftMin = MathF.Min(useXAxis ? leftStart.X : leftStart.Y, useXAxis ? leftEnd.X : leftEnd.Y);
float leftMax = MathF.Max(useXAxis ? leftStart.X : leftStart.Y, useXAxis ? leftEnd.X : leftEnd.Y);
float rightMin = MathF.Min(useXAxis ? rightStart.X : rightStart.Y, useXAxis ? rightEnd.X : rightEnd.Y);
float rightMax = MathF.Max(useXAxis ? rightStart.X : rightStart.Y, useXAxis ? rightEnd.X : rightEnd.Y);
float overlapMin = MathF.Max(leftMin, rightMin);
float overlapMax = MathF.Min(leftMax, rightMax);
if (overlapMax + sideEpsilon < overlapMin)
{
clippedCount = 0;
return false;
}
float t0 = (overlapMin - baseOrigin) / baseExtent;
float t1 = (overlapMax - baseOrigin) / baseExtent;
StoreLinearIntersection(baseStart + t0 * baseDelta, baseStart + t1 * baseDelta,
distanceEpsilonSq, clipped, out clippedCount);
return true;
}
float t = Cross2D(offset, rightDelta) / cross;
float u = Cross2D(offset, leftDelta) / cross;
if (t < -parameterEpsilon || t > 1.0f + parameterEpsilon ||
u < -parameterEpsilon || u > 1.0f + parameterEpsilon)
{
clippedCount = 0;
return false;
}
t = Math.Clamp(t, 0.0f, 1.0f);
clipped[0] = leftStart + t * leftDelta;
clippedCount = 1;
return true;
}
private static bool TryClipLinearIntersection(ClipPoint[] left, int leftCount,
ClipPoint[] right, int rightCount,
float sideEpsilon, float distanceEpsilonSq, float areaEpsilon,
ClipPoint[] clipped, out int clippedCount)
{
if (leftCount < 2 || rightCount < 2)
{
clippedCount = 0;
return false;
}
if (leftCount == 2 && rightCount == 2)
{
return IntersectSegments(left[0], left[1], right[0], right[1],
sideEpsilon, distanceEpsilonSq, areaEpsilon, clipped, out clippedCount);
}
if (leftCount == 2)
{
return ClipSegmentAgainstPolygon(left[0], left[1], right, rightCount,
sideEpsilon, distanceEpsilonSq, clipped, out clippedCount);
}
if (rightCount == 2)
{
return ClipSegmentAgainstPolygon(right[0], right[1], left, leftCount,
sideEpsilon, distanceEpsilonSq, clipped, out clippedCount);
}
clippedCount = 0;
return false;
}
private static void ReducePolygon(ClipPoint[] polygon, ref int count)
{
if (count <= FinalVertexLimit) return;
ClipPoint[] reduced = new ClipPoint[FinalVertexLimit];
for (int i = 0; i < FinalVertexLimit; i++)
{
int index = ((2 * i + 1) * count) / (2 * FinalVertexLimit);
reduced[i] = polygon[index];
}
Array.Copy(reduced, polygon, FinalVertexLimit);
count = FinalVertexLimit;
}
}
================================================
FILE: other/ContactClipping/JitterClipVisualizer.csproj
================================================
Exe
net10.0
disable
enable
================================================
FILE: other/ContactClipping/Program.cs
================================================
using System;
using System.Numerics;
using Raylib_cs;
namespace JitterClipVisualizer;
internal static class Program
{
private static readonly Color Background = new(246, 241, 233, 255);
private static readonly Color PanelFill = new(255, 252, 246, 255);
private static readonly Color PanelBorder = new(205, 191, 170, 255);
private static readonly Color Ink = new(37, 45, 57, 255);
private static readonly Color Muted = new(110, 120, 132, 255);
private static readonly Color SubjectFill = new(35, 101, 176, 64);
private static readonly Color SubjectStroke = new(35, 101, 176, 255);
private static readonly Color ClipFill = new(230, 142, 33, 64);
private static readonly Color ClipStroke = new(230, 142, 33, 255);
private static readonly Color OutputFill = new(37, 151, 92, 88);
private static readonly Color OutputStroke = new(37, 151, 92, 255);
private static readonly Color InputStroke = new(118, 128, 140, 255);
private static readonly Color CurrentEdge = new(198, 61, 63, 255);
private static int presetIndex;
private static int stepIndex;
private static float sceneTime;
private static float stepTimer;
private static bool animateGeometry = true;
private static bool animateSteps = true;
private static void Main()
{
Raylib.SetConfigFlags(ConfigFlags.ResizableWindow | ConfigFlags.Msaa4xHint);
Raylib.InitWindow(1500, 940, "Jitter2 ContactManifold Clipping Visualizer");
Raylib.SetTargetFPS(60);
var presets = ContactManifoldClipDebugger.CreatePresets();
while (!Raylib.WindowShouldClose())
{
float deltaTime = Raylib.GetFrameTime();
HandleInput(presets.Count);
if (animateGeometry)
{
sceneTime += deltaTime;
}
ClipSnapshot snapshot = ContactManifoldClipDebugger.BuildSnapshot(presets[presetIndex], sceneTime);
UpdateCurrentStep(snapshot, deltaTime);
Raylib.BeginDrawing();
Raylib.ClearBackground(Background);
DrawScene(snapshot);
Raylib.EndDrawing();
}
Raylib.CloseWindow();
}
private static void HandleInput(int presetCount)
{
if (Raylib.IsKeyPressed(KeyboardKey.Space))
{
animateSteps = !animateSteps;
}
if (Raylib.IsKeyPressed(KeyboardKey.A))
{
animateGeometry = !animateGeometry;
}
if (Raylib.IsKeyPressed(KeyboardKey.R))
{
sceneTime = 0.0f;
stepIndex = 0;
stepTimer = 0.0f;
}
if (Raylib.IsKeyPressed(KeyboardKey.Tab))
{
presetIndex = (presetIndex + 1) % presetCount;
stepIndex = 0;
stepTimer = 0.0f;
}
if (Raylib.IsKeyPressed(KeyboardKey.One))
{
presetIndex = 0;
stepIndex = 0;
stepTimer = 0.0f;
}
if (Raylib.IsKeyPressed(KeyboardKey.Two) && presetCount > 1)
{
presetIndex = 1;
stepIndex = 0;
stepTimer = 0.0f;
}
if (Raylib.IsKeyPressed(KeyboardKey.Three) && presetCount > 2)
{
presetIndex = 2;
stepIndex = 0;
stepTimer = 0.0f;
}
if (Raylib.IsKeyPressed(KeyboardKey.Right))
{
animateSteps = false;
stepIndex += 1;
}
if (Raylib.IsKeyPressed(KeyboardKey.Left))
{
animateSteps = false;
stepIndex -= 1;
}
}
private static void UpdateCurrentStep(ClipSnapshot snapshot, float deltaTime)
{
int stepCount = Math.Max(snapshot.Steps.Length, 1);
if (animateSteps && stepCount > 1)
{
stepTimer += deltaTime;
if (stepTimer >= 0.95f)
{
stepTimer -= 0.95f;
stepIndex = (stepIndex + 1) % stepCount;
}
}
if (stepIndex < 0)
{
stepIndex = stepCount - 1;
}
if (stepIndex >= stepCount)
{
stepIndex = 0;
}
}
private static void DrawScene(ClipSnapshot snapshot)
{
int width = Raylib.GetScreenWidth();
int height = Raylib.GetScreenHeight();
const float outerMargin = 24.0f;
const float columnGap = 18.0f;
const float panelTop = 124.0f;
const float panelBottom = 88.0f;
float panelWidth = (width - outerMargin * 2.0f - columnGap * 2.0f) / 3.0f;
float panelHeight = height - panelTop - panelBottom;
Rectangle leftPanel = new(outerMargin, panelTop, panelWidth, panelHeight);
Rectangle centerPanel = new(outerMargin + panelWidth + columnGap, panelTop, panelWidth, panelHeight);
Rectangle rightPanel = new(outerMargin + (panelWidth + columnGap) * 2.0f, panelTop, panelWidth, panelHeight);
ClipStep step = snapshot.Steps[stepIndex];
WorldBounds bounds = CollectBounds(snapshot, step);
DrawHeader(snapshot);
DrawPanel(leftPanel, "Projected Inputs");
DrawPanel(centerPanel, "Current Pass");
DrawPanel(rightPanel, "Final Output");
DrawGeometryPanel(leftPanel, bounds, snapshot.Left, snapshot.Right, snapshot.Result);
DrawStepPanel(centerPanel, bounds, snapshot, step);
DrawResultPanel(rightPanel, bounds, snapshot.Left, snapshot.Right, snapshot.Result);
DrawFooter(snapshot);
}
private static void DrawHeader(ClipSnapshot snapshot)
{
Raylib.DrawText(snapshot.PresetName, 28, 18, 34, Ink);
Raylib.DrawText(snapshot.Description, 28, 57, 22, Muted);
string source = "Mirrors the 2D clipping helpers in src/Jitter2/Collision/NarrowPhase/CollisionManifold.cs";
Raylib.DrawText(source, 28, 85, 18, Muted);
string status = $"{ContactManifoldClipDebugger.GetModeLabel(snapshot.Mode)} | {snapshot.Status}";
Raylib.DrawText(status, 28, 106, 18, Ink);
}
private static void DrawFooter(ClipSnapshot snapshot)
{
int y = Raylib.GetScreenHeight() - 54;
string controls = "Tab/1-3 preset Left/Right step Space autoplay steps A animate geometry R reset time Esc quit";
string step = $"Step {stepIndex + 1}/{Math.Max(snapshot.Steps.Length, 1)}";
Raylib.DrawText(controls, 28, y, 18, Muted);
Raylib.DrawText(step, Raylib.GetScreenWidth() - 170, y, 18, Ink);
}
private static void DrawPanel(Rectangle rect, string title)
{
Raylib.DrawRectangleRounded(rect, 0.03f, 8, PanelFill);
Raylib.DrawRectangleRoundedLinesEx(rect, 0.03f, 8, 1.4f, PanelBorder);
Raylib.DrawText(title, (int)rect.X + 16, (int)rect.Y + 12, 24, Ink);
}
private static void DrawGeometryPanel(Rectangle rect, WorldBounds bounds, ClipPoint[] left, ClipPoint[] right, ClipPoint[] result)
{
Rectangle plot = GetPlotRect(rect);
DrawGrid(plot, bounds);
DrawShape(plot, bounds, left, SubjectFill, SubjectStroke, 2.4f);
DrawShape(plot, bounds, right, ClipFill, ClipStroke, 2.4f);
if (result.Length > 0)
{
DrawShape(plot, bounds, result, Raylib.Fade(OutputFill, 0.70f), OutputStroke, 2.2f);
}
DrawLegend((int)rect.X + 16, (int)rect.Y + 46, SubjectStroke, "subject");
DrawLegend((int)rect.X + 124, (int)rect.Y + 46, ClipStroke, "clip");
DrawLegend((int)rect.X + 206, (int)rect.Y + 46, OutputStroke, "result");
}
private static void DrawStepPanel(Rectangle rect, WorldBounds bounds, ClipSnapshot snapshot, ClipStep step)
{
Rectangle plot = GetPlotRect(rect);
DrawGrid(plot, bounds);
DrawShape(plot, bounds, snapshot.Right, ClipFill, ClipStroke, 2.2f);
DrawShape(plot, bounds, step.InputPolygon, new Color(160, 166, 175, 34), InputStroke, 2.2f);
DrawShape(plot, bounds, step.OutputPolygon, OutputFill, OutputStroke, 2.6f);
if (step.EdgeIndex >= 0)
{
DrawEdge(plot, bounds, step.EdgeStart, step.EdgeEnd, CurrentEdge, 4.0f);
}
int textX = (int)rect.X + 16;
int textY = (int)(rect.Y + rect.Height) - 88;
Raylib.DrawText(step.Title, textX, textY, 22, Ink);
Raylib.DrawText(step.Summary, textX, textY + 28, 18, Muted);
}
private static void DrawResultPanel(Rectangle rect, WorldBounds bounds, ClipPoint[] left, ClipPoint[] right, ClipPoint[] result)
{
Rectangle plot = GetPlotRect(rect);
DrawGrid(plot, bounds);
DrawShape(plot, bounds, left, new Color(0, 0, 0, 0), SubjectStroke, 2.0f);
DrawShape(plot, bounds, right, new Color(0, 0, 0, 0), ClipStroke, 2.0f);
DrawShape(plot, bounds, result, OutputFill, OutputStroke, 3.0f);
string label = result.Length switch
{
0 => "empty result",
1 => "point contact",
2 => "line overlap",
_ => "area overlap"
};
Raylib.DrawText(label, (int)rect.X + 16, (int)(rect.Y + rect.Height) - 54, 20, Ink);
}
private static void DrawGrid(Rectangle plot, WorldBounds bounds)
{
Raylib.DrawRectangleRounded(plot, 0.02f, 6, new Color(250, 247, 241, 255));
Vector2 left = WorldToScreen(plot, bounds, new ClipPoint(bounds.MinX, 0.0f));
Vector2 right = WorldToScreen(plot, bounds, new ClipPoint(bounds.MaxX, 0.0f));
Vector2 top = WorldToScreen(plot, bounds, new ClipPoint(0.0f, bounds.MaxY));
Vector2 bottom = WorldToScreen(plot, bounds, new ClipPoint(0.0f, bounds.MinY));
Raylib.DrawLineEx(left, right, 1.0f, new Color(226, 219, 206, 255));
Raylib.DrawLineEx(top, bottom, 1.0f, new Color(226, 219, 206, 255));
}
private static void DrawLegend(int x, int y, Color color, string text)
{
Raylib.DrawRectangle(x, y + 3, 16, 10, color);
Raylib.DrawText(text, x + 22, y, 18, Muted);
}
private static Rectangle GetPlotRect(Rectangle panel)
{
return new Rectangle(panel.X + 14.0f, panel.Y + 78.0f, panel.Width - 28.0f, panel.Height - 166.0f);
}
private static void DrawShape(Rectangle plot, WorldBounds bounds, ClipPoint[] polygon, Color fill, Color stroke, float thickness)
{
if (polygon.Length == 0) return;
Vector2[] vertices = new Vector2[polygon.Length];
for (int i = 0; i < polygon.Length; i++)
{
vertices[i] = WorldToScreen(plot, bounds, polygon[i]);
}
if (polygon.Length >= 3 && fill.A > 0)
{
for (int i = 1; i < vertices.Length - 1; i++)
{
Raylib.DrawTriangle(vertices[0], vertices[i], vertices[i + 1], fill);
}
}
if (polygon.Length == 1)
{
Raylib.DrawCircleV(vertices[0], 6.0f, stroke);
return;
}
for (int i = 0; i < vertices.Length - 1; i++)
{
Raylib.DrawLineEx(vertices[i], vertices[i + 1], thickness, stroke);
}
if (polygon.Length >= 3)
{
Raylib.DrawLineEx(vertices[^1], vertices[0], thickness, stroke);
}
foreach (Vector2 vertex in vertices)
{
Raylib.DrawCircleV(vertex, 4.5f, stroke);
Raylib.DrawCircleV(vertex, 2.2f, PanelFill);
}
}
private static void DrawEdge(Rectangle plot, WorldBounds bounds, ClipPoint start, ClipPoint end, Color color, float thickness)
{
Vector2 a = WorldToScreen(plot, bounds, start);
Vector2 b = WorldToScreen(plot, bounds, end);
Raylib.DrawLineEx(a, b, thickness, color);
Raylib.DrawCircleV(a, 5.0f, color);
Raylib.DrawCircleV(b, 5.0f, color);
}
private static Vector2 WorldToScreen(Rectangle plot, WorldBounds bounds, ClipPoint point)
{
float rangeX = MathF.Max(bounds.MaxX - bounds.MinX, 1.0f);
float rangeY = MathF.Max(bounds.MaxY - bounds.MinY, 1.0f);
float scale = MathF.Min((plot.Width - 36.0f) / rangeX, (plot.Height - 36.0f) / rangeY);
float centerX = 0.5f * (bounds.MinX + bounds.MaxX);
float centerY = 0.5f * (bounds.MinY + bounds.MaxY);
return new Vector2(
plot.X + plot.Width * 0.5f + (point.X - centerX) * scale,
plot.Y + plot.Height * 0.5f - (point.Y - centerY) * scale);
}
private static WorldBounds CollectBounds(ClipSnapshot snapshot, ClipStep step)
{
WorldBounds bounds = new(float.MaxValue, float.MaxValue, float.MinValue, float.MinValue);
Expand(ref bounds, snapshot.Left);
Expand(ref bounds, snapshot.Right);
Expand(ref bounds, snapshot.Result);
Expand(ref bounds, step.InputPolygon);
Expand(ref bounds, step.OutputPolygon);
if (step.EdgeIndex >= 0)
{
Expand(ref bounds, step.EdgeStart);
Expand(ref bounds, step.EdgeEnd);
}
if (bounds.MinX == float.MaxValue)
{
bounds = new WorldBounds(-120.0f, -120.0f, 120.0f, 120.0f);
}
float sizeX = bounds.MaxX - bounds.MinX;
float sizeY = bounds.MaxY - bounds.MinY;
float pad = MathF.Max(MathF.Max(sizeX, sizeY) * 0.16f, 24.0f);
return new WorldBounds(bounds.MinX - pad, bounds.MinY - pad, bounds.MaxX + pad, bounds.MaxY + pad);
}
private static void Expand(ref WorldBounds bounds, ClipPoint point)
{
bounds.MinX = MathF.Min(bounds.MinX, point.X);
bounds.MinY = MathF.Min(bounds.MinY, point.Y);
bounds.MaxX = MathF.Max(bounds.MaxX, point.X);
bounds.MaxY = MathF.Max(bounds.MaxY, point.Y);
}
private static void Expand(ref WorldBounds bounds, ClipPoint[] points)
{
foreach (ClipPoint point in points)
{
Expand(ref bounds, point);
}
}
private struct WorldBounds(float minX, float minY, float maxX, float maxY)
{
public float MinX = minX;
public float MinY = minY;
public float MaxX = maxX;
public float MaxY = maxY;
}
}
================================================
FILE: other/GodotDemo/.gitattributes
================================================
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
================================================
FILE: other/GodotDemo/.gitignore
================================================
# Godot 4+ specific ignores
.godot/
.idea
================================================
FILE: other/GodotDemo/JitterGodot.csproj
================================================
net8.0
true
================================================
FILE: other/GodotDemo/JitterGodot.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JitterGodot", "JitterGodot.csproj", "{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal
================================================
FILE: other/GodotDemo/Program.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using Jitter2;
using Jitter2.Collision.Shapes;
using Jitter2.Dynamics;
using Jitter2.Dynamics.Constraints;
using Jitter2.LinearMath;
using Jitter2.SoftBodies;
public static class Conversion
{
public static Vector3 FromJitter(in JVector vec) => new Vector3(vec.X, vec.Y, vec.Z);
}
public partial class JitterCubes : MultiMeshInstance3D
{
private List cubes = new();
public void AddCube(RigidBody body)
{
cubes.Add(body);
}
public void Clear() => cubes.Clear();
public override void _Ready()
{
Multimesh = new MultiMesh();
Multimesh.TransformFormat = MultiMesh.TransformFormatEnum.Transform3D;
Multimesh.Mesh = new BoxMesh();
Multimesh.Mesh.SurfaceSetMaterial(0, ResourceLoader.Load("res://box.material"));
base._Ready();
}
public override void _Process(double delta)
{
Multimesh.InstanceCount = cubes.Count;
for (int i = 0; i < cubes.Count; i++)
{
JMatrix mat = JMatrix.CreateFromQuaternion(cubes[i].Data.Orientation);
JVector pos = cubes[i].Data.Position;
Transform3D trans = Transform3D.Identity;
trans[0] = Conversion.FromJitter(mat.GetColumn(0));
trans[1] = Conversion.FromJitter(mat.GetColumn(1));
trans[2] = Conversion.FromJitter(mat.GetColumn(2));
trans[3] = Conversion.FromJitter(pos);
Multimesh.SetInstanceTransform(i, trans);
}
base._Process(delta);
}
}
public partial class Program : Node3D
{
private World world = null!;
private JitterCubes jitterCubes = null!;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
var button = new Button();
button.Pressed += ResetScene;
button.Position = new Vector2(4, 4);
button.Text = "Reset scene";
jitterCubes = new JitterCubes();
AddChild(jitterCubes);
AddChild(button);
world = new World();
ResetScene();
}
private void ResetScene()
{
jitterCubes.Clear();
world.Clear();
// floor shape
RigidBody floor = world.CreateRigidBody();
floor.AddShape(new BoxShape(40));
floor.Position = new JVector(0, -20, 0);
floor.IsStatic = true;
for (int i = 0; i < 30; i++)
{
RigidBody body = world.CreateRigidBody();
body.AddShape(new BoxShape(1));
body.Position = new JVector(1, 0.5f + i * 4, 0);
body.Friction = 0.2f;
jitterCubes.AddCube(body);
}
}
float accumulatedTime = 0.0f;
public override void _Process(double delta)
{
const float fixedStep = 1.0f / 100.0f;
int steps = 0;
accumulatedTime += (float)delta;
while (accumulatedTime > fixedStep)
{
world.Step(fixedStep, true);
accumulatedTime -= fixedStep;
// we can not keep up with the real time, i.e. the simulation
// is running slower than the real time is passing.
if (++steps >= 4) return;
}
}
}
================================================
FILE: other/GodotDemo/README.md
================================================
## Jitter2 in Godot
Small demo to get started with Jitter2 in Godot.
Get Godot (.NET, Version >= 4.3) from https://godotengine.org/. Open Godot and select "project.godot".
================================================
FILE: other/GodotDemo/assets/box.png.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bmwn361sv2cuo"
path.s3tc="res://.godot/imported/box.png-52cc4e41fddadef836f9ad4dbe7936fa.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://assets/box.png"
dest_files=["res://.godot/imported/box.png-52cc4e41fddadef836f9ad4dbe7936fa.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
================================================
FILE: other/GodotDemo/assets/floor.png.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bmalqw2x4nhkw"
path.s3tc="res://.godot/imported/floor.png-38406586736af2fe805d7b886727ee47.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://assets/floor.png"
dest_files=["res://.godot/imported/floor.png-38406586736af2fe805d7b886727ee47.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
================================================
FILE: other/GodotDemo/icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bj3qla3sx2dm8"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: other/GodotDemo/main_scene.tscn
================================================
[gd_scene load_steps=10 format=3 uid="uid://coyqv4677vx2l"]
[ext_resource type="Texture2D" uid="uid://bmalqw2x4nhkw" path="res://assets/floor.png" id="1_cx0yb"]
[ext_resource type="Script" path="res://Program.cs" id="1_q0tim"]
[ext_resource type="Material" uid="uid://drmul300eph46" path="res://box.material" id="2_ht1to"]
[sub_resource type="PlaneMesh" id="PlaneMesh_bdasd"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6v48f"]
albedo_color = Color(0.34902, 0.505882, 0.717647, 1)
albedo_texture = ExtResource("1_cx0yb")
metallic = 0.29
metallic_specular = 0.54
roughness = 0.65
uv1_scale = Vector3(10, 10, 10)
[sub_resource type="PhysicalSkyMaterial" id="PhysicalSkyMaterial_hoxee"]
ground_color = Color(0.729412, 0.627451, 0.52549, 1)
energy_multiplier = 4.35
[sub_resource type="Sky" id="Sky_u73ht"]
sky_material = SubResource("PhysicalSkyMaterial_hoxee")
[sub_resource type="Environment" id="Environment_ekotd"]
background_mode = 2
sky = SubResource("Sky_u73ht")
ambient_light_energy = 2.86
[sub_resource type="BoxMesh" id="BoxMesh_0u3bq"]
[node name="Node3D" type="Node3D"]
script = ExtResource("1_q0tim")
[node name="Floor" type="MeshInstance3D" parent="."]
transform = Transform3D(20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0)
mesh = SubResource("PlaneMesh_bdasd")
surface_material_override/0 = SubResource("StandardMaterial3D_6v48f")
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(0.966823, -0.0320158, 0.253432, 0, 0.992115, 0.125333, -0.255446, -0.121175, 0.9592, 2.65486, 4.6383, 9.92122)
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_ekotd")
[node name="TestCube" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.49664, 0)
visible = false
mesh = SubResource("BoxMesh_0u3bq")
surface_material_override/0 = ExtResource("2_ht1to")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.574853, -0.657468, 0.487113, 0.0171256, 0.604843, 0.796161, -0.818077, -0.449333, 0.358955, 7.26371, 4.83944, 0)
light_energy = 1.61
shadow_enabled = true
================================================
FILE: other/GodotDemo/project.godot
================================================
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="JitterGodot"
run/main_scene="res://main_scene.tscn"
config/features=PackedStringArray("4.3", "C#", "Forward Plus")
config/icon="res://icon.svg"
[display]
window/size/viewport_width=1200
window/size/viewport_height=800
[dotnet]
project/assembly_name="JitterGodot"
[rendering]
anti_aliasing/quality/msaa_3d=2
anti_aliasing/quality/use_taa=true
================================================
FILE: other/GodotSoftBodies/.gitattributes
================================================
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
================================================
FILE: other/GodotSoftBodies/.gitignore
================================================
# Godot 4+ specific ignores
.godot/
.idea
================================================
FILE: other/GodotSoftBodies/JitterGodot.csproj
================================================
net8.0
true
================================================
FILE: other/GodotSoftBodies/JitterGodot.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JitterGodot", "JitterGodot.csproj", "{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{D95E4F8D-CDCA-48E7-95EC-3EBBD7A0431A}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal
================================================
FILE: other/GodotSoftBodies/Program.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using Jitter2;
using Jitter2.Collision.Shapes;
using Jitter2.Dynamics;
using Jitter2.Dynamics.Constraints;
using Jitter2.LinearMath;
using Jitter2.SoftBodies;
public static class Conversion
{
public static Vector3 FromJitter(in JVector vec) => new Vector3(vec.X, vec.Y, vec.Z);
}
public class CubedSoftBody(World world) : SoftBody(world)
{
public int MaterialIndex { get; set; } = 0;
public struct BuildVertex(int x, int y, int z)
{
public int X = x, Y = y, Z = z;
public static BuildVertex operator +(BuildVertex a, BuildVertex b)
=> new BuildVertex(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
}
public struct BuildTetrahedron(int a, int b, int c, int d)
{
public int A = a, B = b, C = c, D = d;
}
public class BuildQuad(int a, int b, int c, int d, bool diagLeft = false) : IEquatable
{
public int A = a, B = b, C = c, D = d;
public bool DiagLeft = diagLeft;
public Vector2 UvA;
public Vector2 UvB;
public Vector2 UvC;
public Vector2 UvD;
public void SetUvCoordinates(BuildVertex[] vertices)
{
int minX = Math.Min(Math.Min(Math.Min(vertices[A].X, vertices[B].X), vertices[C].X), vertices[D].X);
int minY = Math.Min(Math.Min(Math.Min(vertices[A].Y, vertices[B].Y), vertices[C].Y), vertices[D].Y);
int minZ = Math.Min(Math.Min(Math.Min(vertices[A].Z, vertices[B].Z), vertices[C].Z), vertices[D].Z);
if (vertices[A].X == vertices[B].X && vertices[A].X == vertices[C].X && vertices[A].X == vertices[D].X)
{
UvA = new Vector2(vertices[A].Y - minY, vertices[A].Z - minZ) * 0.5f;
UvB = new Vector2(vertices[B].Y - minY, vertices[B].Z - minZ) * 0.5f;
UvC = new Vector2(vertices[C].Y - minY, vertices[C].Z - minZ) * 0.5f;
UvD = new Vector2(vertices[D].Y - minY, vertices[D].Z - minZ) * 0.5f;
}
else if (vertices[A].Y == vertices[B].Y && vertices[A].Y == vertices[C].Y && vertices[A].Y == vertices[D].Y)
{
UvA = new Vector2(vertices[A].X - minX, vertices[A].Z - minZ) * 0.5f;
UvB = new Vector2(vertices[B].X - minX, vertices[B].Z - minZ) * 0.5f;
UvC = new Vector2(vertices[C].X - minX, vertices[C].Z - minZ) * 0.5f;
UvD = new Vector2(vertices[D].X - minX, vertices[D].Z - minZ) * 0.5f;
}
else if (vertices[A].Z == vertices[B].Z && vertices[A].Z == vertices[C].Z && vertices[A].Z == vertices[D].Z)
{
UvA = new Vector2(vertices[A].X - minX, vertices[A].Y - minY) * 0.5f;
UvB = new Vector2(vertices[B].X - minX, vertices[B].Y - minY) * 0.5f;
UvC = new Vector2(vertices[C].X - minX, vertices[C].Y - minY) * 0.5f;
UvD = new Vector2(vertices[D].X - minX, vertices[D].Y - minY) * 0.5f;
}
else
{
throw new InvalidOperationException();
}
}
public override bool Equals(object obj)
{
return obj is BuildQuad other && Equals(other);
}
public bool Equals(BuildQuad other)
{
bool bA = (A == other.A || A == other.B || A == other.C || A == other.D);
bool bB = (B == other.A || B == other.B || B == other.C || B == other.D);
bool bC = (C == other.A || C == other.B || C == other.C || C == other.D);
bool bD = (D == other.A || D == other.B || D == other.C || D == other.D);
return bA && bB && bC && bD;
}
public override int GetHashCode()
{
return (A * B * C * D) + (A + B + C + D);
}
}
private bool isFinished = false;
private readonly Dictionary vertexIndices = new();
private List tetrahedra = new();
private List cubeCenters = new();
public HashSet Quads { get; } = new();
private BuildVertex[] cubeVertices = new[]
{
new BuildVertex(+1, -1, +1),
new BuildVertex(+1, -1, -1),
new BuildVertex(-1, -1, -1),
new BuildVertex(-1, -1, +1),
new BuildVertex(+1, +1, +1),
new BuildVertex(+1, +1, -1),
new BuildVertex(-1, +1, -1),
new BuildVertex(-1, +1, +1)
};
private int PushVertex(BuildVertex vertex)
{
if (vertexIndices.TryGetValue(vertex, out int result))
return result;
result = vertexIndices.Count;
vertexIndices.Add(vertex, result);
return result;
}
public void AddCube(int x, int y, int z)
{
if (isFinished) throw new InvalidOperationException();
var origin = new BuildVertex(x, y, z);
cubeCenters.Add(origin);
Span idx = stackalloc int[8];
for (int i = 0; i < 8; i++)
{
idx[i] = PushVertex(origin + cubeVertices[i]);
}
tetrahedra.Add(new BuildTetrahedron(idx[0], idx[1], idx[5], idx[2]));
tetrahedra.Add(new BuildTetrahedron(idx[5], idx[2], idx[6], idx[7]));
tetrahedra.Add(new BuildTetrahedron(idx[0], idx[3], idx[2], idx[7]));
tetrahedra.Add(new BuildTetrahedron(idx[4], idx[0], idx[5], idx[7]));
tetrahedra.Add(new BuildTetrahedron(idx[0], idx[2], idx[5], idx[7]));
void PushQuad(BuildQuad bq)
{
if (!Quads.Add(bq)) Quads.Remove(bq);
}
PushQuad(new BuildQuad(idx[5], idx[4], idx[7], idx[6], true)); // top
PushQuad(new BuildQuad(idx[2], idx[3], idx[0], idx[1], false)); // bottom
PushQuad(new BuildQuad(idx[6], idx[7], idx[3], idx[2], true)); // left
PushQuad(new BuildQuad(idx[1], idx[0], idx[4], idx[5], false)); // right
PushQuad(new BuildQuad(idx[3], idx[7], idx[4], idx[0], true)); // front
PushQuad(new BuildQuad(idx[1], idx[5], idx[6], idx[2], false)); // back
}
public void Finalize(float scale = 0.25f)
{
if (isFinished) throw new InvalidOperationException();
BuildVertex[] vertices = vertexIndices.OrderBy(pair => pair.Value).Select(pair => pair.Key).ToArray();
foreach (var quad in Quads)
{
quad.SetUvCoordinates(vertices);
}
foreach (var vertex in vertices)
{
var rb = world.CreateRigidBody();
rb.SetMassInertia(JMatrix.Zero, 8f, true);
rb.Position = new JVector(vertex.X, vertex.Y, vertex.Z) * scale;
rb.Damping = (0.02f, 0.002f);
this.Vertices.Add(rb);
}
foreach (var trh in tetrahedra)
{
SoftBodyTetrahedron sbt = new(this,
Vertices[trh.A], Vertices[trh.B],
Vertices[trh.C], Vertices[trh.D]);
world.DynamicTree.AddProxy(sbt);
this.Shapes.Add(sbt);
}
for (int i = 0; i < cubeCenters.Count; i++)
{
var centerCube = world.CreateRigidBody();
centerCube.Position = new JVector(cubeCenters[i].X, cubeCenters[i].Y, cubeCenters[i].Z) * scale;
centerCube.SetMassInertia(JMatrix.Identity * 1f, 1f);
List ct = new(8);
ct.Add(world.CreateConstraint(centerCube, Vertices[tetrahedra[5 * i + 0].A])); // 0
ct.Add(world.CreateConstraint(centerCube, Vertices[tetrahedra[5 * i + 0].B])); // 1
ct.Add(world.CreateConstraint(centerCube, Vertices[tetrahedra[5 * i + 0].C])); // 5
ct.Add(world.CreateConstraint(centerCube, Vertices[tetrahedra[5 * i + 0].D])); // 2
ct.Add(world.CreateConstraint(centerCube, Vertices[tetrahedra[5 * i + 1].C])); // 6
ct.Add(world.CreateConstraint(centerCube, Vertices[tetrahedra[5 * i + 1].D])); // 7
ct.Add(world.CreateConstraint(centerCube, Vertices[tetrahedra[5 * i + 2].B])); // 3
ct.Add(world.CreateConstraint(centerCube, Vertices[tetrahedra[5 * i + 3].A])); // 4
foreach (var c in ct)
{
c.Initialize(c.Body2.Position);
c.Softness = 0.1f;
c.Bias = 0.3f;
}
}
isFinished = true;
}
}
public partial class JitterSoftBodyCubeDrawer : MeshInstance3D
{
private ImmediateMesh immediateMesh = new();
private List cubes = new();
private Material[] materials = new Material[5];
public void Clear()
{
foreach (var cube in cubes)
{
cube.Destroy();
}
cubes.Clear();
}
public void AddCubedSoftBody(CubedSoftBody body)
{
cubes.Add(body);
}
public override void _Ready()
{
this.Mesh = immediateMesh;
var mat = ResourceLoader.Load("res://box.material");
for (int i = 0; i < 5; i++)
{
materials[i] = mat.Duplicate(false) as Material;
}
((StandardMaterial3D)materials[0]).AlbedoColor = new Color(0, 0.94f, 0.94f); // I
((StandardMaterial3D)materials[1]).AlbedoColor = new Color(0.94f, 0.94f, 0); // O
((StandardMaterial3D)materials[2]).AlbedoColor = new Color(0, 0, 0.94f); // L
((StandardMaterial3D)materials[3]).AlbedoColor = new Color(0.94f ,0, 0); // Z
((StandardMaterial3D)materials[4]).AlbedoColor = new Color(0.63f, 0, 0.94f); // T
base._Ready();
}
public override void _Process(double delta)
{
int surfaces = 0;
immediateMesh.ClearSurfaces();
foreach (var cube in cubes)
{
immediateMesh.SurfaceBegin(Mesh.PrimitiveType.Triangles);
foreach (var quad in cube.Quads)
{
if (quad.DiagLeft)
{
immediateMesh.SurfaceSetUV(quad.UvA);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.A].Position));
immediateMesh.SurfaceSetUV(quad.UvB);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.B].Position));
immediateMesh.SurfaceSetUV(quad.UvC);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.C].Position));
immediateMesh.SurfaceSetUV(quad.UvA);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.A].Position));
immediateMesh.SurfaceSetUV(quad.UvC);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.C].Position));
immediateMesh.SurfaceSetUV(quad.UvD);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.D].Position));
}
else
{
immediateMesh.SurfaceSetUV(quad.UvA);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.A].Position));
immediateMesh.SurfaceSetUV(quad.UvB);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.B].Position));
immediateMesh.SurfaceSetUV(quad.UvD);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.D].Position));
immediateMesh.SurfaceSetUV(quad.UvB);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.B].Position));
immediateMesh.SurfaceSetUV(quad.UvC);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.C].Position));
immediateMesh.SurfaceSetUV(quad.UvD);
immediateMesh.SurfaceAddVertex(Conversion.FromJitter(cube.Vertices[quad.D].Position));
}
}
immediateMesh.SurfaceEnd();
immediateMesh.SurfaceSetMaterial(surfaces++, materials[cube.MaterialIndex]);
}
base._Process(delta);
}
}
public partial class Program : Node3D
{
private World world = null!;
private JitterSoftBodyCubeDrawer softBodyDrawer = new();
private void Add2x2x2(CubedSoftBody csb, int x, int y, int z)
{
csb.AddCube(x + 1, y + 1, z + 1);
csb.AddCube(x + 1, y + 1, z - 1);
csb.AddCube(x - 1, y + 1, z + 1);
csb.AddCube(x - 1, y + 1, z - 1);
csb.AddCube(x + 1, y - 1, z + 1);
csb.AddCube(x + 1, y - 1, z - 1);
csb.AddCube(x - 1, y - 1, z + 1);
csb.AddCube(x - 1, y - 1, z - 1);
}
private void AddI(int x, int y, int z)
{
CubedSoftBody csb = new CubedSoftBody(world);
Add2x2x2(csb, x, y + 0, z);
Add2x2x2(csb, x, y + 4, z);
Add2x2x2(csb, x, y + 8, z);
Add2x2x2(csb, x, y + 12, z);
csb.Finalize();
csb.MaterialIndex = 0;
softBodyDrawer.AddCubedSoftBody(csb);
}
private void AddO(int x, int y, int z)
{
CubedSoftBody csb = new CubedSoftBody(world);
Add2x2x2(csb, x + 0, y + 0, z);
Add2x2x2(csb, x + 0, y + 4, z);
Add2x2x2(csb, x + 4, y + 0, z);
Add2x2x2(csb, x + 4, y + 4, z);
csb.Finalize();
csb.MaterialIndex = 1;
softBodyDrawer.AddCubedSoftBody(csb);
}
private void AddL(int x, int y, int z)
{
CubedSoftBody csb = new CubedSoftBody(world);
Add2x2x2(csb, x + 0, y + 0, z);
Add2x2x2(csb, x + 0, y + 4, z);
Add2x2x2(csb, x + 4, y + 4, z);
Add2x2x2(csb, x + 8, y + 4, z);
csb.Finalize();
csb.MaterialIndex = 2;
softBodyDrawer.AddCubedSoftBody(csb);
}
private void AddZ(int x, int y, int z)
{
CubedSoftBody csb = new CubedSoftBody(world);
Add2x2x2(csb, x + 0, y + 0, z);
Add2x2x2(csb, x + 4, y + 0, z);
Add2x2x2(csb, x + 4, y + 4, z);
Add2x2x2(csb, x + 8, y + 4, z);
csb.Finalize();
csb.MaterialIndex = 3;
softBodyDrawer.AddCubedSoftBody(csb);
}
private void AddT(int x, int y, int z)
{
CubedSoftBody csb = new CubedSoftBody(world);
Add2x2x2(csb, x + 0, y + 0, z);
Add2x2x2(csb, x + 4, y + 0, z);
Add2x2x2(csb, x + 8, y + 0, z);
Add2x2x2(csb, x + 4, y + 4, z);
csb.Finalize();
csb.MaterialIndex = 4;
softBodyDrawer.AddCubedSoftBody(csb);
}
public override void _Ready()
{
var button = new Button();
button.Pressed += ResetScene;
button.Position = new Vector2(4, 4);
button.Text = "Reset scene";
AddChild(button);
AddChild(softBodyDrawer);
world = new World();
ResetScene();
}
private void ResetScene()
{
softBodyDrawer.Clear();
world.Clear();
// floor shape
RigidBody floor = world.CreateRigidBody();
floor.AddShape(new BoxShape(40));
floor.Position = new JVector(0, -20, 0);
floor.IsStatic = true;
world.DynamicTree.Filter = DynamicTreeCollisionFilter.Filter;
world.BroadPhaseFilter = new BroadPhaseCollisionFilter(world);
for (int i = 0; i < 5; i++)
{
AddI(0, (5 * i + 0) * 30, 0);
AddL(0, (5 * i + 1) * 30, 0);
AddZ(0, (5 * i + 2) * 30, 0);
AddT(0, (5 * i + 3) * 30, 0);
AddO(0, (5 * i + 4) * 30, 0);
}
world.SubstepCount = 4;
world.SolverIterations = (4, 1);
}
float accumulatedTime = 0.0f;
public override void _Process(double delta)
{
const float fixedStep = 1.0f / 100.0f;
int steps = 0;
accumulatedTime += (float)delta;
while (accumulatedTime > fixedStep)
{
world.Step(fixedStep, true);
accumulatedTime -= fixedStep;
// we can not keep up with the real time, i.e. the simulation
// is running slower than the real time is passing.
if (++steps >= 4) return;
}
}
}
================================================
FILE: other/GodotSoftBodies/README.md
================================================
## Jitter2 Soft Bodies in Godot
Small demo to showing Jitter2 soft bodies in Godot.
Get Godot (.NET, Version >= 4.3) from https://godotengine.org/. Open Godot and select "project.godot".
================================================
FILE: other/GodotSoftBodies/assets/texture_08.png.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://couisnncdt24n"
path.s3tc="res://.godot/imported/texture_08.png-55422b6c9181f85f6ecd7904cb7603ac.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://assets/texture_08.png"
dest_files=["res://.godot/imported/texture_08.png-55422b6c9181f85f6ecd7904cb7603ac.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
================================================
FILE: other/GodotSoftBodies/icon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bj3qla3sx2dm8"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: other/GodotSoftBodies/main_scene.tscn
================================================
[gd_scene load_steps=9 format=3 uid="uid://coyqv4677vx2l"]
[ext_resource type="Script" path="res://Program.cs" id="1_q0tim"]
[ext_resource type="Material" uid="uid://bnmu1bn4m70pu" path="res://box.material" id="4_umvs4"]
[sub_resource type="PlaneMesh" id="PlaneMesh_bdasd"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6v48f"]
albedo_color = Color(0.694118, 0.756863, 0.976471, 1)
uv1_scale = Vector3(10, 10, 10)
[sub_resource type="PhysicalSkyMaterial" id="PhysicalSkyMaterial_hoxee"]
ground_color = Color(0.627451, 0.607843, 0.890196, 1)
energy_multiplier = 4.35
[sub_resource type="Sky" id="Sky_u73ht"]
sky_material = SubResource("PhysicalSkyMaterial_hoxee")
[sub_resource type="Environment" id="Environment_ekotd"]
background_mode = 2
sky = SubResource("Sky_u73ht")
ambient_light_energy = 2.86
[sub_resource type="BoxMesh" id="BoxMesh_0u3bq"]
[node name="Node3D" type="Node3D"]
script = ExtResource("1_q0tim")
[node name="Floor" type="MeshInstance3D" parent="."]
transform = Transform3D(20, 0, 0, 0, 20, 0, 0, 0, 20, 0, 0, 0)
mesh = SubResource("PlaneMesh_bdasd")
surface_material_override/0 = SubResource("StandardMaterial3D_6v48f")
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(0.956687, -0.036487, 0.288824, 0, 0.992115, 0.125333, -0.29112, -0.119904, 0.949143, 4.44888, 6.19976, 15.391)
visible = false
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_ekotd")
[node name="TestCube" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.49664, 0)
visible = false
mesh = SubResource("BoxMesh_0u3bq")
surface_material_override/0 = ExtResource("4_umvs4")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.574853, -0.657468, 0.487113, 0.0171256, 0.604843, 0.796161, -0.818077, -0.449333, 0.358955, 7.26371, 4.83944, 0)
shadow_enabled = true
================================================
FILE: other/GodotSoftBodies/project.godot
================================================
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="JitterGodot"
run/main_scene="res://main_scene.tscn"
config/features=PackedStringArray("4.3", "C#", "Forward Plus")
config/icon="res://icon.svg"
[display]
window/size/viewport_width=1920
window/size/viewport_height=1200
[dotnet]
project/assembly_name="JitterGodot"
[rendering]
lights_and_shadows/directional_shadow/size=8192
anti_aliasing/quality/msaa_3d=3
================================================
FILE: other/WebDemo/LICENSE
================================================
The WebDemo uses the outstanding code from
https://github.com/disketteman/DotnetRaylibWasm
and
https://github.com/Kiriller12/RaylibWasm
to setup a wasm demo in the browser. License below.
MIT License
Copyright (c) 2022 Stan
Copyright (c) 2023 Kiriller12
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The demo code itself is a modified demo (fog) from raylib
https://github.com/raysan5/raylib
License below.
Copyright (c) 2013-2023 Ramon Santamaria (@raysan5)
This software is provided "as-is", without any express or implied warranty. In no event
will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial
applications, and to alter it and redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you
wrote the original software. If you use this software in a product, an acknowledgment
in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented
as being the original software.
3. This notice may not be removed or altered from any source distribution.
================================================
FILE: other/WebDemo/Program.cs
================================================
using System;
using System.Collections.Generic;
using System.Numerics;
using Raylib_cs;
using Jitter2;
using Jitter2.Collision;
using Jitter2.Collision.Shapes;
using Jitter2.Dynamics;
using Jitter2.Dynamics.Constraints;
using Jitter2.LinearMath;
using Jitter2.SoftBodies;
using static Raylib_cs.Raylib;
using static Raylib_cs.Rlgl;
using System.Runtime.InteropServices.JavaScript;
namespace WebDemo;
// ─── Render metadata stored in body.Tag ──────────────────────────────────────
public enum BodyShape { Box, Sphere }
public record struct RenderTag(BodyShape Shape, float Sx, float Sy, float Sz);
// ─── Scene interface ──────────────────────────────────────────────────────────
public interface IScene
{
string Name { get; }
string Hint { get; }
void Build(Playground pg);
}
// ─── Pressurized soft-body sphere ────────────────────────────────────────────
sealed class PressurizedSphere : SoftBody
{
const float Pressure = 280f;
private sealed class UnitSphere : ISupportMappable
{
public void SupportMap(in JVector direction, out JVector result)
=> result = JVector.Normalize(direction);
public void GetCenter(out JVector point)
=> throw new NotImplementedException();
}
public PressurizedSphere(World world, JVector center, float radius = 1f) : base(world)
{
var tris = ShapeHelper.Tessellate(new UnitSphere(), 3);
var vertMap = new Dictionary();
var triIndices = new List<(int, int, int)>();
var edgeSet = new HashSet<(int, int)>();
foreach (var tri in tris)
{
JVector[] pts = [tri.V0, tri.V1, tri.V2];
int[] idx = new int[3];
for (int k = 0; k < 3; k++)
{
if (!vertMap.TryGetValue(pts[k], out idx[k]))
{
idx[k] = vertMap.Count;
vertMap[pts[k]] = idx[k];
var body = world.CreateRigidBody();
body.Position = center + pts[k] * radius;
body.SetMassInertia(JMatrix.Zero, 15f, true);
body.Damping = (0.0004f, 0f);
Vertices.Add(body);
}
}
triIndices.Add((idx[0], idx[1], idx[2]));
for (int k = 0; k < 3; k++)
{
int a = idx[k], b = idx[(k + 1) % 3];
edgeSet.Add(a < b ? (a, b) : (b, a));
}
}
foreach (var (a, b) in edgeSet)
{
var spring = world.CreateConstraint(Vertices[a], Vertices[b]);
spring.Initialize(Vertices[a].Position, Vertices[b].Position);
spring.Softness = 0.5f;
Springs.Add(spring);
}
foreach (var (i0, i1, i2) in triIndices)
{
var shape = new SoftBodyTriangle(this, Vertices[i0], Vertices[i1], Vertices[i2]);
World.DynamicTree.AddProxy(shape);
Shapes.Add(shape);
}
}
protected override void WorldOnPostStep(float dt)
{
base.WorldOnPostStep(dt);
if (!IsActive) return;
float volume = 0f;
foreach (SoftBodyTriangle tri in Shapes)
{
JVector v1 = tri.Vertex1.Position, v2 = tri.Vertex2.Position, v3 = tri.Vertex3.Position;
volume += ((v2.Y - v1.Y) * (v3.Z - v1.Z) - (v2.Z - v1.Z) * (v3.Y - v1.Y)) * (v1.X + v2.X + v3.X);
}
float invVol = 1f / MathF.Max(0.1f, volume);
foreach (SoftBodyTriangle tri in Shapes)
{
JVector p0 = tri.Vertex1.Position, p1 = tri.Vertex2.Position, p2 = tri.Vertex3.Position;
JVector normal = (p1 - p0) % (p2 - p0);
JVector force = normal * Pressure * invVol;
const float maxForce = 2f;
float fl2 = force.LengthSquared();
if (fl2 > maxForce * maxForce) force *= maxForce / MathF.Sqrt(fl2);
tri.Vertex1.AddForce(force, false);
tri.Vertex2.AddForce(force, false);
tri.Vertex3.AddForce(force, false);
}
}
}
// ─── Scenes ──────────────────────────────────────────────────────────────────
sealed class SceneSoftBall : IScene
{
public string Name => "Soft Body";
public string Hint => "Pressurized sphere rolls down a ramp";
public PressurizedSphere? Sphere { get; private set; }
public void Build(Playground pg)
{
pg.World.SubstepCount = 2;
pg.World.DynamicTree.Filter = DynamicTreeCollisionFilter.Filter;
pg.World.BroadPhaseFilter = new BroadPhaseCollisionFilter(pg.World);
pg.AddGround();
// Tilted ramp (~30°)
var ramp = pg.AddBox(new JVector(0f, 3f, 0f), 9f, 0.4f, 3.5f);
ramp.MotionType = MotionType.Static;
ramp.Orientation = JQuaternion.CreateRotationZ(-0.52f);
ramp.Friction = 0.6f;
// Small 2D pyramid
int[] layerN = [3, 2, 1];
for (int layer = 0; layer < layerN.Length; layer++)
{
int n = layerN[layer];
float zOff = layer * 0.5f;
for (int iz = 0; iz < n; iz++)
pg.AddBox(new JVector(6f, 0.5f + layer, -1f + zOff + iz));
}
Sphere = new PressurizedSphere(pg.World, new JVector(-3f, 10f, 0f));
}
}
// ─────────────────────────────────────────────────────────────────────────────
sealed class SceneTower : IScene
{
public string Name => "Plank Tower";
public string Hint => "Stacked plank tower stress test";
public void Build(Playground pg)
{
pg.World.SolverIterations = (8, 4);
pg.World.SubstepCount = 3;
pg.AddGround();
static void AddPlank(Playground pg, float x, float y, float z, float sx, float sy, float sz)
{
var body = pg.AddBox(new JVector(x, y, z), sx, sy, sz);
body.Friction = 0.4f;
}
static void BuildBlock(Playground pg, Vector3 halfExtents, Vector3 shift, int numx, int numy, int numz)
{
Vector3[] dimensions = [halfExtents, new Vector3(halfExtents.Z, halfExtents.Y, halfExtents.X)];
float blockWidth = 2.0f * halfExtents.Z * numx;
float blockHeight = 2.0f * halfExtents.Y * numy;
float spacing = (halfExtents.Z * numx - halfExtents.X) / (numz - 1.0f);
int nx = numx;
int nz = numz;
for (int i = 0; i < numy; i++)
{
(nx, nz) = (nz, nx);
Vector3 dim = dimensions[i % 2];
float y = dim.Y * i * 2.0f;
for (int j = 0; j < nx; j++)
{
float x = (i % 2 == 0) ? spacing * j * 2.0f : dim.X * j * 2.0f;
for (int k = 0; k < nz; k++)
{
float z = (i % 2 == 0) ? dim.Z * k * 2.0f : spacing * k * 2.0f;
AddPlank(pg,
x + dim.X + shift.X,
y + dim.Y + shift.Y,
z + dim.Z + shift.Z,
dim.X * 2.0f,
dim.Y * 2.0f,
dim.Z * 2.0f);
}
}
}
Vector3 topDim = new(halfExtents.Z, halfExtents.X, halfExtents.Y);
int topNx = (int)(blockWidth / (topDim.X * 2.0f));
int topNz = (int)(blockWidth / (topDim.Z * 2.0f));
for (int i = 0; i < topNx; i++)
for (int j = 0; j < topNz; j++)
AddPlank(pg,
i * topDim.X * 2.0f + topDim.X + shift.X,
topDim.Y + shift.Y + blockHeight,
j * topDim.Z * 2.0f + topDim.Z + shift.Z,
topDim.X * 2.0f,
topDim.Y * 2.0f,
topDim.Z * 2.0f);
}
Vector3 halfExtents = new(0.1f, 0.65f, 2.0f);
int[] layerCounts = [0, 3, 5, 7, 9, 0];
float stackBaseHeight = 0.0f;
for (int i = 3; i >= 1; i--)
{
int numx = i;
int numy = layerCounts[i];
int numz = numx * 3 + 1;
float blockWidth = numx * halfExtents.Z * 2.0f;
BuildBlock(
pg,
halfExtents,
new Vector3(-blockWidth / 2.0f, stackBaseHeight, -blockWidth / 2.0f),
numx,
numy,
numz);
stackBaseHeight += numy * halfExtents.Y * 2.0f + halfExtents.X * 2.0f;
}
var ball = pg.AddSphere(new JVector(0f, 5f, -18f), 1.2f);
ball.SetMassInertia(12f);
ball.Restitution = 0.15f;
ball.Friction = 0.7f;
ball.Velocity = new JVector(0f, 0f, 38f);
}
}
// ─────────────────────────────────────────────────────────────────────────────
sealed class SceneDominoes : IScene
{
public string Name => "Dominoes";
public string Hint => "Toppling domino chain";
// domino geometry
const float Sp = 0.72f; // spacing
const float DH = 2.0f; // height
const float DW = 0.9f; // width
const float DT = 0.15f; // thickness
const float DY = DH * 0.5f;
public void Build(Playground pg)
{
pg.AddGround();
void Add(float x, float z, float angle)
{
var d = pg.AddBox(new JVector(x, DY, z), DT, DH, DW);
d.Orientation = JQuaternion.CreateRotationY(angle);
d.Friction = 0.5f;
}
// ── Segment 1: straight along +X ─────────────────────────────
const int n1 = 9;
const float s1x = -7f, s1z = -7f;
for (int i = 0; i < n1; i++)
Add(s1x + i * Sp, s1z, 0f);
// ── Arc 1: 90° left turn (+X → +Z), R=3, CCW ─────────────────
float a1cx = s1x + n1 * Sp, a1cz = s1z + 3f;
const int na1 = 7;
for (int i = 1; i <= na1; i++)
{
float phi = -MathF.PI / 2f + i * (MathF.PI / 2f) / (na1 + 1);
Add(a1cx + 3f * MathF.Cos(phi), a1cz + 3f * MathF.Sin(phi), -(phi + MathF.PI / 2f));
}
// ── Segment 2: straight along +Z ─────────────────────────────
const int n2 = 9;
float s2x = a1cx + 3f, s2z = a1cz;
for (int i = 0; i < n2; i++)
Add(s2x, s2z + i * Sp, -MathF.PI / 2f);
// ── Arc 2: 90° right turn (+Z → +X), R=3, CW ─────────────────
float a2cx = s2x + 3f, a2cz = s2z + n2 * Sp;
const int na2 = 7;
for (int i = 1; i <= na2; i++)
{
float phi = MathF.PI - i * (MathF.PI / 2f) / (na2 + 1);
Add(a2cx + 3f * MathF.Cos(phi), a2cz + 3f * MathF.Sin(phi), MathF.PI / 2f - phi);
}
// ── Segment 3: straight along +X ─────────────────────────────
const int n3 = 9;
float s3x = a2cx, s3z = a2cz + 3f;
for (int i = 0; i < n3; i++)
Add(s3x + i * Sp, s3z, 0f);
// ── Trigger ball ──────────────────────────────────────────────
var ball = pg.AddSphere(new JVector(s1x - 3.5f, 0.9f, s1z), 0.9f);
ball.SetMassInertia(3f);
ball.Restitution = 0.1f;
ball.Friction = 0.8f;
ball.Velocity = new JVector(4f, 0f, 0f);
ball.AngularVelocity = new JVector(0f, 0f, -4.4f);
}
}
// ─────────────────────────────────────────────────────────────────────────────
sealed class SceneJenga : IScene
{
public string Name => "Jenga";
public string Hint => "Projectile tests a classic stacked tower";
public void Build(Playground pg)
{
pg.World.SolverIterations = (8, 4);
pg.World.SubstepCount = 3;
pg.AddGround();
const float bD = 0.8f, bH = 0.8f, bW = 3f * bD; // bW = 3*bD keeps both layer orientations identical
const int layers = 22;
for (int layer = 0; layer < layers; layer++)
{
float y = bH * 0.5f + layer * bH;
if (layer % 2 == 0)
for (int i = -1; i <= 1; i++)
pg.AddBox(new JVector(i * bD, y, 0f), bD, bH, bW);
else
for (int i = -1; i <= 1; i++)
pg.AddBox(new JVector(0f, y, i * bD), bW, bH, bD);
}
var projectile = pg.AddSphere(new JVector(-12f, 3.5f, 0f), 0.9f);
projectile.SetMassInertia(14f);
projectile.Restitution = 0.15f;
projectile.Velocity = new JVector(28f, 0f, 0f);
}
}
// ─────────────────────────────────────────────────────────────────────────────
sealed class SceneWreckingBall : IScene
{
public const float BallRadius = 1.2f;
public string Name => "Wrecking Ball";
public string Hint => "Pendulum demolishes a tower";
public JVector AnchorPos;
public RigidBody Ball = null!;
public void Build(Playground pg)
{
pg.AddGround();
// Box tower on the right
for (int ix = 0; ix < 3; ix++)
for (int iz = 0; iz < 3; iz++)
for (int iy = 0; iy < 8; iy++)
pg.AddBox(new JVector(5f + ix * 1.0f, 0.5f + iy, iz * 1.0f - 1.0f));
// Static anchor point above and left
AnchorPos = new JVector(-1f, 14f, 0f);
var anchor = pg.World.CreateRigidBody();
anchor.Position = AnchorPos;
anchor.MotionType = MotionType.Static;
// Heavy ball: rope attaches at sphere surface toward anchor
const float RopeLength = 10f;
const float PullAngle = MathF.PI * 0.25f; // 45° from vertical
var ropeDir = new JVector(-MathF.Sin(PullAngle), -MathF.Cos(PullAngle), 0f);
JVector constraintPt = AnchorPos + ropeDir * RopeLength; // on ball surface
var ball = pg.AddSphere(AnchorPos + ropeDir * (RopeLength + BallRadius), BallRadius);
ball.SetMassInertia(40f);
ball.Restitution = 0.2f;
Ball = ball;
var rope = pg.World.CreateConstraint(anchor, ball);
rope.Initialize(AnchorPos, constraintPt, LinearLimit.Fixed);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── Playground ──────────────────────────────────────────────────────────────
public class Playground
{
private const float FixedTimeStep = 1f / 100f;
private const int MaxSimulationStepsPerFrame = 4;
private const float GroundSize = 40f;
private static readonly (float angular, float linear) DemoDeactivationThreshold = (0.02f, 0.02f);
private const int UiTitleFontSize = 26;
private const int UiBodyFontSize = 16;
private const int UiSmallFontSize = 15;
private const float UiFontOversample = 2.0f;
private Camera3D _camera;
private Shader _shader;
private Model _modelBox, _modelSphere, _modelGround, _modelRope;
private Texture2D _texFull, _texQuad, _texBlue, _texYellow, _texGray;
private Font _fontTitle, _fontBody, _fontSmall;
private bool _disposed;
private float _accumulatedTime;
private bool _hasPendingPointerTap;
private Vector2 _pendingPointerTap;
public World World { get; private set; } = null!;
private readonly List _scenes = new()
{
new SceneSoftBall(),
new SceneTower(),
new SceneDominoes(),
new SceneJenga(),
new SceneWreckingBall(),
};
private int _sceneIndex;
private IScene Current => _scenes[_sceneIndex];
private static readonly Color SceneBgDark = new(28, 30, 36, 255);
private static readonly Color SceneBgLight = new(238, 243, 248, 255);
private Color _sceneBackground = SceneBgDark;
private Color _panelBackground = new(18, 20, 26, 255);
private Color _tabActive = new(91, 168, 245, 255);
private Color _tabInactive = new(50, 50, 50, 255);
private Color _hintColor = new(180, 180, 180, 255);
private Color _ctrlColor = new(130, 130, 130, 255);
public Playground()
{
SetConfigFlags(ConfigFlags.Msaa4xHint);
InitWindow(800, 600, "Jitter2 Physics Demo");
_camera = new Camera3D
{
Position = new Vector3(2f, 12f, -22f),
Target = new Vector3(0f, 4f, 0f),
Up = new Vector3(0f, 1f, 0f),
FovY = 45f,
Projection = CameraProjection.Perspective
};
_modelBox = LoadModelFromMesh(GenMeshCube(1f, 1f, 1f));
_modelSphere = LoadModelFromMesh(GenMeshSphere(0.5f, 24, 24));
_modelGround = LoadModelFromMesh(GenMeshPlane(GroundSize, GroundSize, 1, 1));
_modelRope = LoadModelFromMesh(GenMeshCylinder(1f, 1f, 12));
var img = LoadImage("assets/texel_checker.png");
_texFull = LoadTextureFromImage(img);
_texQuad = LoadTextureFromImage(ImageFromImage(img, new Rectangle(0, 0, 512, 512)));
UnloadImage(img);
var imgBlue = GenImageChecked(1024, 1024, 256, 256, new Color(30, 90, 165, 255), new Color(55, 130, 210, 255));
_texBlue = LoadTextureFromImage(imgBlue);
UnloadImage(imgBlue);
var imgYellow = GenImageChecked(1024, 1024, 128, 128, new Color(180, 140, 20, 255), new Color(235, 195, 50, 255));
_texYellow = LoadTextureFromImage(imgYellow);
UnloadImage(imgYellow);
var imgGray = GenImageColor(16, 16, new Color(90, 93, 100, 255));
_texGray = LoadTextureFromImage(imgGray);
UnloadImage(imgGray);
SetMaterialTexture(ref _modelBox, 0, MaterialMapIndex.Albedo, ref _texBlue);
SetMaterialTexture(ref _modelSphere, 0, MaterialMapIndex.Albedo, ref _texYellow);
SetMaterialTexture(ref _modelGround, 0, MaterialMapIndex.Albedo, ref _texFull);
SetMaterialTexture(ref _modelRope, 0, MaterialMapIndex.Albedo, ref _texGray);
_shader = LoadShader("assets/lighting.vs", "assets/lighting.fs");
unsafe
{
_shader.Locs[(int)ShaderLocationIndex.MatrixModel] = GetShaderLocation(_shader, "matModel");
_shader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(_shader, "viewPos");
}
int ambLoc = GetShaderLocation(_shader, "ambient");
SetShaderValue(_shader, ambLoc, new float[] { 0.25f, 0.25f, 0.25f, 1f }, ShaderUniformDataType.Vec4);
SetMaterialShader(ref _modelBox, 0, ref _shader);
SetMaterialShader(ref _modelSphere, 0, ref _shader);
SetMaterialShader(ref _modelGround, 0, ref _shader);
SetMaterialShader(ref _modelRope, 0, ref _shader);
Rlights.CreateLight(0, LightType.Point, new Vector3(0f, 14f, -8f), Vector3.Zero, Color.White, _shader);
_fontTitle = LoadFontEx("assets/JetBrainsMono-Regular.ttf", GetAtlasFontSize(UiTitleFontSize), null, 0);
_fontBody = LoadFontEx("assets/JetBrainsMono-Regular.ttf", GetAtlasFontSize(UiBodyFontSize), null, 0);
_fontSmall = LoadFontEx("assets/JetBrainsMono-Regular.ttf", GetAtlasFontSize(UiSmallFontSize), null, 0);
SetTextureFilter(_fontTitle.Texture, TextureFilter.Bilinear);
SetTextureFilter(_fontBody.Texture, TextureFilter.Bilinear);
SetTextureFilter(_fontSmall.Texture, TextureFilter.Bilinear);
SetTargetFPS(60);
LoadScene(0);
}
// ─── Scene management ────────────────────────────────────────────────────
private void LoadScene(int index)
{
_sceneIndex = index;
World?.Dispose();
World = new World();
World.Gravity = new JVector(0f, -9.81f, 0f);
Current.Build(this);
}
// ─── Scene helpers ───────────────────────────────────────────────────────
public RigidBody AddGround(float y = 0f)
{
var body = World.CreateRigidBody();
body.AddShape(new BoxShape(GroundSize, 2f, GroundSize));
body.Position = new JVector(0f, y - 1f, 0f);
body.MotionType = MotionType.Static;
// no Tag → not drawn by DrawBodies
return body;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
World?.Dispose();
UnloadFont(_fontTitle);
UnloadFont(_fontBody);
UnloadFont(_fontSmall);
UnloadModel(_modelBox);
UnloadModel(_modelSphere);
UnloadModel(_modelGround);
UnloadModel(_modelRope);
UnloadTexture(_texQuad);
UnloadTexture(_texFull);
UnloadTexture(_texBlue);
UnloadTexture(_texYellow);
UnloadTexture(_texGray);
UnloadShader(_shader);
}
public RigidBody AddBox(JVector pos, float sx = 1f, float sy = 1f, float sz = 1f)
{
var body = World.CreateRigidBody();
body.Position = pos;
body.AddShape(new BoxShape(sx, sy, sz));
body.DeactivationThreshold = DemoDeactivationThreshold;
body.Tag = new RenderTag(BodyShape.Box, sx, sy, sz);
return body;
}
public RigidBody AddSphere(JVector pos, float radius = 0.5f)
{
var body = World.CreateRigidBody();
body.AddShape(new SphereShape(radius));
body.Position = pos;
body.DeactivationThreshold = DemoDeactivationThreshold;
float d = radius * 2f;
body.Tag = new RenderTag(BodyShape.Sphere, d, d, d);
return body;
}
public void SetTheme(bool lightTheme)
{
_sceneBackground = lightTheme ? SceneBgLight : SceneBgDark;
_panelBackground = lightTheme ? new Color(34, 48, 60, 255) : new Color(18, 20, 26, 255);
_tabActive = lightTheme ? new Color(91, 168, 245, 255) : new Color(91, 168, 245, 255);
_tabInactive = lightTheme ? new Color(78, 96, 114, 255) : new Color(50, 50, 50, 255);
_hintColor = lightTheme ? new Color(214, 224, 235, 255) : new Color(180, 180, 180, 255);
_ctrlColor = lightTheme ? new Color(176, 193, 209, 255) : new Color(130, 130, 130, 255);
}
// ─── Frame ───────────────────────────────────────────────────────────────
public void UpdateFrame()
{
HandleInput();
Viewport(0, 0, GetRenderWidth(), GetRenderHeight());
unsafe
{
SetShaderValue(_shader,
_shader.Locs[(int)ShaderLocationIndex.VectorView],
_camera.Position, ShaderUniformDataType.Vec3);
}
UpdateCamera(ref _camera, CameraMode.Orbital);
BeginDrawing();
ClearBackground(_sceneBackground);
BeginMode3D(_camera);
DrawGround();
DrawBodies();
DrawExtras();
EndMode3D();
DrawUI();
EndDrawing();
_accumulatedTime += GetFrameTime();
int steps = 0;
while (_accumulatedTime >= FixedTimeStep && steps < MaxSimulationStepsPerFrame)
{
World.Step(FixedTimeStep, false);
_accumulatedTime -= FixedTimeStep;
steps++;
}
if (steps == MaxSimulationStepsPerFrame && _accumulatedTime > FixedTimeStep)
{
_accumulatedTime = FixedTimeStep;
}
}
// ─── Input ───────────────────────────────────────────────────────────────
private void HandleInput()
{
if (IsKeyPressed(KeyboardKey.Right))
LoadScene((_sceneIndex + 1) % _scenes.Count);
else if (IsKeyPressed(KeyboardKey.Left))
LoadScene((_sceneIndex + _scenes.Count - 1) % _scenes.Count);
else if (IsKeyPressed(KeyboardKey.R))
LoadScene(_sceneIndex);
if (_hasPendingPointerTap)
{
_hasPendingPointerTap = false;
HandleTabPress(_pendingPointerTap);
}
}
private void HandleTabPress(Vector2 pointer)
{
const int tabH = 40;
int sw = GetScreenWidth();
int sh = GetScreenHeight();
int tabY = sh - tabH;
if (pointer.Y >= tabY && pointer.Y < sh)
{
int tabW = sw / _scenes.Count;
int index = Math.Clamp((int)(pointer.X / tabW), 0, _scenes.Count - 1);
if (index != _sceneIndex) LoadScene(index);
}
}
public void QueuePointerTap(float x, float y)
{
_pendingPointerTap = new Vector2(x, y);
_hasPendingPointerTap = true;
}
// ─── Drawing ─────────────────────────────────────────────────────────────
private void DrawGround()
{
_modelGround.Transform = Matrix4x4.Identity;
DrawModel(_modelGround, Vector3.Zero, 1f, new Color(200, 200, 200, 255));
}
private void DrawBodies()
{
foreach (var body in World.RigidBodies)
{
if (body.Tag is not RenderTag tag) continue;
var m = BuildTransform(body, tag.Sx, tag.Sy, tag.Sz);
switch (tag.Shape)
{
case BodyShape.Box:
_modelBox.Transform = m;
DrawModel(_modelBox, Vector3.Zero, 1f, Color.White);
break;
case BodyShape.Sphere:
_modelSphere.Transform = m;
DrawModel(_modelSphere, Vector3.Zero, 1f, Color.White);
break;
}
}
}
private void DrawExtras()
{
// Soft Body: draw triangles
if (Current is SceneSoftBall sb && sb.Sphere is { } sphere)
{
var col = new Color(60, 220, 80, 255);
foreach (SoftBodyTriangle tri in sphere.Shapes)
{
Vector3 p1 = new(tri.Vertex1.Position.X, tri.Vertex1.Position.Y, tri.Vertex1.Position.Z);
Vector3 p2 = new(tri.Vertex2.Position.X, tri.Vertex2.Position.Y, tri.Vertex2.Position.Z);
Vector3 p3 = new(tri.Vertex3.Position.X, tri.Vertex3.Position.Y, tri.Vertex3.Position.Z);
DrawLine3D(p1, p2, col);
DrawLine3D(p2, p3, col);
DrawLine3D(p3, p1, col);
}
}
// Wrecking Ball: draw rope
if (Current is SceneWreckingBall wb && wb.Ball is { } ball)
{
Vector3 a = new(wb.AnchorPos.X, wb.AnchorPos.Y, wb.AnchorPos.Z);
Vector3 center = new(ball.Position.X, ball.Position.Y, ball.Position.Z);
Vector3 toAnchor = Vector3.Normalize(a - center);
Vector3 b = center + toAnchor * SceneWreckingBall.BallRadius;
Vector3 delta = b - a;
float length = delta.Length();
if (length > 1e-4f)
{
Vector3 dir = delta / length;
Vector3 axis = Vector3.Cross(Vector3.UnitY, dir);
float dot = Math.Clamp(Vector3.Dot(Vector3.UnitY, dir), -1f, 1f);
float angle = MathF.Acos(dot) * 180f / MathF.PI;
if (axis.LengthSquared() < 1e-6f)
{
axis = Vector3.UnitX;
if (dot > 0f) angle = 0f;
}
else
{
axis = Vector3.Normalize(axis);
}
DrawModelEx(
_modelRope,
a,
axis,
angle,
new Vector3(0.05f, length, 0.05f),
Color.Yellow);
}
DrawSphere(a, 0.15f, Color.Gray);
}
}
// ─── UI overlay ──────────────────────────────────────────────────────────
private Font SelectFont(float size)
{
if (size >= UiTitleFontSize - 1f) return _fontTitle;
if (size >= UiBodyFontSize - 1f) return _fontBody;
return _fontSmall;
}
private static int GetAtlasFontSize(int uiSize)
{
return (int)MathF.Ceiling(uiSize * UiFontOversample);
}
private void DrawTextF(string text, float x, float y, float size, Color color)
{
Font font = SelectFont(size);
float drawSize = MathF.Round(size);
float spacing = MathF.Round(drawSize / 20f);
Vector2 position = new(MathF.Round(x), MathF.Round(y));
DrawTextEx(font, text, position, drawSize, spacing, color);
}
private float MeasureTextF(string text, float size)
{
Font font = SelectFont(size);
float drawSize = MathF.Round(size);
float spacing = MathF.Round(drawSize / 20f);
return MeasureTextEx(font, text, drawSize, spacing).X;
}
private void DrawUI()
{
int sw = GetScreenWidth();
int sh = GetScreenHeight();
float titleSize = 26f;
float hintSize = 16f;
float ctrlSize = 15f;
float tabLabelSize = 15f;
int topBarHeight = 54;
int tabH = 40;
DrawRectangle(0, 0, sw, topBarHeight, _panelBackground);
DrawTextF(Current.Name, 12, 7, titleSize, Color.White);
DrawTextF(Current.Hint, 12, 34, hintSize, _hintColor);
string ctrl = $"Arrow Keys to Switch Scene R Reset {GetFPS()} fps";
DrawTextF(ctrl, sw - MeasureTextF(ctrl, ctrlSize) - 10, 10, ctrlSize, _ctrlColor);
DrawRectangle(0, sh - tabH, sw, tabH, _panelBackground);
int tabW = sw / _scenes.Count;
for (int i = 0; i < _scenes.Count; i++)
{
int tx = i * tabW;
DrawRectangle(tx + 1, sh - tabH + 2, tabW - 2, tabH - 4,
i == _sceneIndex ? _tabActive : _tabInactive);
string label = _scenes[i].Name;
float lw = MeasureTextF(label, tabLabelSize);
DrawTextF(label, tx + (tabW - lw) / 2f, sh - tabH + 13, tabLabelSize, Color.White);
}
}
// ─── Math ────────────────────────────────────────────────────────────────
static Matrix4x4 BuildTransform(RigidBody body, float sx, float sy, float sz)
{
JMatrix r = JMatrix.CreateFromQuaternion(body.Orientation);
JVector p = body.Position;
return new Matrix4x4(
r.M11 * sx, r.M12 * sy, r.M13 * sz, p.X,
r.M21 * sx, r.M22 * sy, r.M23 * sz, p.Y,
r.M31 * sx, r.M32 * sy, r.M33 * sz, p.Z,
0f, 0f, 0f, 1f);
}
}
// ─── Application entry point ──────────────────────────────────────────────────
public partial class Application
{
private static Playground _playground = null!;
[JSExport] public static void UpdateFrame() => _playground.UpdateFrame();
[JSExport] public static void Resize(int w, int h) => SetWindowSize(w, h);
[JSExport] public static void SetTheme(bool lightTheme) => _playground.SetTheme(lightTheme);
[JSExport] public static void PointerTap(float x, float y) => _playground.QueuePointerTap(x, y);
[JSExport] public static void Shutdown() => _playground.Dispose();
public static void Main() => _playground = new Playground();
}
================================================
FILE: other/WebDemo/Properties/AssemblyInfo.cs
================================================
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
[assembly:System.Runtime.Versioning.SupportedOSPlatform("browser")]
================================================
FILE: other/WebDemo/Properties/launchSettings.json
================================================
{
"profiles": {
"WebDemo": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:7022;http://localhost:5269",
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/debug?browser={browserInspectUri}"
}
}
}
================================================
FILE: other/WebDemo/README.md
================================================
## Jitter2 in the browser
**Credits for raylib-cs in the browser and this README go to: https://github.com/disketteman/DotnetRaylibWasm and https://github.com/Kiriller12/RaylibWasm**
## Setup
Make sure you have the latest version of .NET 9.
Install the official wasm tooling:
```
dotnet workload install wasm-tools
dotnet workload install wasm-experimental
```
Install a tool to create ad-hoc http server to serve `application/wasm`:
```
dotnet tool install --global dotnet-serve
```
## Run it
`publish` the solution. Don't use `build`. Publishing may take a while.
```
dotnet publish -c Release
```
To serve the files use this command:
```
dotnet serve --mime .wasm=application/wasm --mime .js=text/javascript --mime .json=application/json --directory ./bin/Release/net9.0/browser-wasm/AppBundle
```
================================================
FILE: other/WebDemo/RLights.cs
================================================
using System.Numerics;
using Raylib_cs;
using static Raylib_cs.Raylib;
public struct Light
{
public bool Enabled;
public LightType Type;
public Vector3 Position;
public Vector3 Target;
public Color Color;
public int EnabledLoc;
public int TypeLoc;
public int PosLoc;
public int TargetLoc;
public int ColorLoc;
}
public enum LightType
{
Directorional,
Point
}
public static class Rlights
{
public static Light CreateLight(
int lightsCount,
LightType type,
Vector3 pos,
Vector3 target,
Color color,
Shader shader
)
{
Light light = new();
light.Enabled = true;
light.Type = type;
light.Position = pos;
light.Target = target;
light.Color = color;
string enabledName = "lights[" + lightsCount + "].enabled";
string typeName = "lights[" + lightsCount + "].type";
string posName = "lights[" + lightsCount + "].position";
string targetName = "lights[" + lightsCount + "].target";
string colorName = "lights[" + lightsCount + "].color";
light.EnabledLoc = GetShaderLocation(shader, enabledName);
light.TypeLoc = GetShaderLocation(shader, typeName);
light.PosLoc = GetShaderLocation(shader, posName);
light.TargetLoc = GetShaderLocation(shader, targetName);
light.ColorLoc = GetShaderLocation(shader, colorName);
UpdateLightValues(shader, light);
return light;
}
private static float[] colors = new float[4];
public static void UpdateLightValues(Shader shader, Light light)
{
// Send to shader light enabled state and type
Raylib.SetShaderValue(
shader,
light.EnabledLoc,
light.Enabled ? 1 : 0,
ShaderUniformDataType.Int
);
Raylib.SetShaderValue(shader, light.TypeLoc, (int)light.Type, ShaderUniformDataType.Int);
// Send to shader light target position values
Raylib.SetShaderValue(shader, light.PosLoc, light.Position, ShaderUniformDataType.Vec3);
// Send to shader light target position values
Raylib.SetShaderValue(shader, light.TargetLoc, light.Target, ShaderUniformDataType.Vec3);
// Send to shader light color values
colors[0] = (float)light.Color.R / 255.0f;
colors[1] = (float)light.Color.G / 255.0f;
colors[2] = (float)light.Color.B / 255.0f;
colors[3] = (float)light.Color.A / 255.0f;
Raylib.SetShaderValue(shader, light.ColorLoc, colors, ShaderUniformDataType.Vec4);
}
}
================================================
FILE: other/WebDemo/WebDemo.csproj
================================================
net9.0
browser-wasm
main.js
Exe
true
true
true
true
true
true
full
true
true
true
-sUSE_GLFW=3
-s USE_GLFW=3 -O3
================================================
FILE: other/WebDemo/assets/JetBrainsMono-LICENSE.txt
================================================
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
================================================
FILE: other/WebDemo/assets/lighting.fs
================================================
#version 100
precision mediump float;
// Input vertex attributes (from vertex shader)
varying vec3 fragPosition;
varying vec2 fragTexCoord;
varying vec4 fragColor;
varying vec3 fragNormal;
// Input uniform values
uniform sampler2D texture0;
uniform vec4 colDiffuse;
// NOTE: Add here your custom variables
#define MAX_LIGHTS 4
#define LIGHT_DIRECTIONAL 0
#define LIGHT_POINT 1
struct MaterialProperty {
vec3 color;
int useSampler;
sampler2D sampler;
};
struct Light {
int enabled;
int type;
vec3 position;
vec3 target;
vec4 color;
};
// Input lighting values
uniform Light lights[MAX_LIGHTS];
uniform vec4 ambient;
uniform vec3 viewPos;
void main()
{
// Texel color fetching from texture sampler
vec4 texelColor = texture2D(texture0, fragTexCoord);
vec3 lightDot = vec3(0.0);
vec3 normal = normalize(fragNormal);
vec3 viewD = normalize(viewPos - fragPosition);
vec3 specular = vec3(0.0);
// NOTE: Implement here your fragment shader code
for (int i = 0; i < MAX_LIGHTS; i++)
{
if (lights[i].enabled == 1)
{
vec3 light = vec3(0.0);
if (lights[i].type == LIGHT_DIRECTIONAL) light = -normalize(lights[i].target - lights[i].position);
if (lights[i].type == LIGHT_POINT) light = normalize(lights[i].position - fragPosition);
float NdotL = max(dot(normal, light), 0.0);
lightDot += lights[i].color.rgb*NdotL;
float specCo = 0.0;
if (NdotL > 0.0) specCo = pow(max(0.0, dot(viewD, reflect(-(light), normal))), 16.0); // Shine: 16.0
specular += specCo;
}
}
vec4 finalColor = (texelColor*((colDiffuse + vec4(specular,1))*vec4(lightDot, 1.0)));
finalColor += texelColor*(ambient/10.0);
// Gamma correction
gl_FragColor = pow(finalColor, vec4(1.0/2.2));
}
================================================
FILE: other/WebDemo/assets/lighting.vs
================================================
#version 100
// Input vertex attributes
attribute vec3 vertexPosition;
attribute vec2 vertexTexCoord;
attribute vec3 vertexNormal;
attribute vec4 vertexColor;
// Input uniform values
uniform mat4 mvp;
uniform mat4 matModel;
// Output vertex attributes (to fragment shader)
varying vec3 fragPosition;
varying vec2 fragTexCoord;
varying vec4 fragColor;
varying vec3 fragNormal;
// NOTE: Add here your custom variables
// https://github.com/glslify/glsl-inverse
mat3 inverse(mat3 m)
{
float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2];
float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2];
float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2];
float b01 = a22*a11 - a12*a21;
float b11 = -a22*a10 + a12*a20;
float b21 = a21*a10 - a11*a20;
float det = a00*b01 + a01*b11 + a02*b21;
return mat3(b01, (-a22*a01 + a02*a21), (a12*a01 - a02*a11),
b11, (a22*a00 - a02*a20), (-a12*a00 + a02*a10),
b21, (-a21*a00 + a01*a20), (a11*a00 - a01*a10))/det;
}
// https://github.com/glslify/glsl-transpose
mat3 transpose(mat3 m)
{
return mat3(m[0][0], m[1][0], m[2][0],
m[0][1], m[1][1], m[2][1],
m[0][2], m[1][2], m[2][2]);
}
void main()
{
// Send vertex attributes to fragment shader
fragPosition = vec3(matModel*vec4(vertexPosition, 1.0));
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
mat3 normalMatrix = transpose(inverse(mat3(matModel)));
fragNormal = normalize(normalMatrix*vertexNormal);
// Calculate final vertex position
gl_Position = mvp*vec4(vertexPosition, 1.0);
}
================================================
FILE: other/WebDemo/index.html
================================================
Jitter2 WebDemo
⭐ View on GitHub
Use ← → to switch scenes and press R to reset.
================================================
FILE: other/WebDemo/main.js
================================================
import { dotnet } from './_framework/dotnet.js';
async function initialize() {
const canvas = document.getElementById('canvas');
const params = new URLSearchParams(window.location.search);
const lightTheme = params.get('theme') === 'light';
// Force an opaque WebGL backbuffer so browser page colors cannot bleed
// through anti-aliased canvas content such as UI text.
const originalGetContext = canvas.getContext.bind(canvas);
canvas.getContext = function(type, attrs) {
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
return originalGetContext(type, {
...attrs,
alpha: false
});
}
return originalGetContext(type, attrs);
};
const { getAssemblyExports, getConfig, runMain } = await dotnet
.withDiagnosticTracing(false)
.create();
const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
dotnet.instance.Module['canvas'] = canvas;
function pointerToCanvasPixels(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const localX = clientX - rect.left;
const localY = clientY - rect.top;
const scaleX = rect.width > 0 ? canvas.width / rect.width : 1;
const scaleY = rect.height > 0 ? canvas.height / rect.height : 1;
return {
x: localX * scaleX,
y: localY * scaleY
};
}
function resizeCanvasToDisplaySize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const cssWidth = canvas.clientWidth;
const cssHeight = canvas.clientHeight;
const pixelWidth = Math.max(1, Math.round(cssWidth * dpr));
const pixelHeight = Math.max(1, Math.round(cssHeight * dpr));
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth;
canvas.height = pixelHeight;
}
exports.WebDemo.Application.Resize(pixelWidth, pixelHeight);
}
function mainLoop() {
resizeCanvasToDisplaySize();
exports.WebDemo.Application.UpdateFrame();
window.requestAnimationFrame(mainLoop);
}
function queuePointerTap(clientX, clientY) {
const point = pointerToCanvasPixels(clientX, clientY);
exports.WebDemo.Application.PointerTap(point.x, point.y);
}
window.addEventListener('resize', resizeCanvasToDisplaySize);
window.addEventListener('orientationchange', resizeCanvasToDisplaySize);
canvas.addEventListener('pointerdown', evt => {
queuePointerTap(evt.clientX, evt.clientY);
if (evt.pointerType === 'touch') {
evt.preventDefault();
}
}, { passive: false });
await runMain();
exports.WebDemo.Application.SetTheme(lightTheme);
resizeCanvasToDisplaySize();
document.getElementById('spinner')?.remove();
window.requestAnimationFrame(mainLoop);
}
initialize().catch(err => {
console.error('An error occurred during initialization:', err);
});
================================================
FILE: other/WebDemo/run
================================================
#!/bin/bash
dotnet publish -c Release && dotnet serve --mime .wasm=application/wasm --mime .js=text/javascript --mime .json=application/json --directory ./bin/Release/net9.0/browser-wasm/AppBundle
================================================
FILE: other/WebDemo/runtimeconfig.template.json
================================================
{
"wasmHostProperties": {
"perHostConfig": [
{
"name": "browser",
"html-path": "index.html",
"Host": "browser"
}
]
}
}
================================================
FILE: src/.gitignore
================================================
.vscode
.vs
imgui.ini
[Bb]in/
[Oo]bj/
BenchmarkDotNet.Artifacts
================================================
FILE: src/Jitter2/Attributes.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
using System;
namespace Jitter2;
///
/// Enum representing reference frames.
///
public enum ReferenceFrame
{
Local,
World
}
///
/// Attribute to specify the reference frame of a member.
///
[AttributeUsage(AttributeTargets.All)]
public sealed class ReferenceFrameAttribute : Attribute
{
///
/// Gets or sets the reference frame.
///
public ReferenceFrame Frame { get; set; }
///
/// Initializes a new instance of the class.
///
/// The reference frame.
public ReferenceFrameAttribute(ReferenceFrame frame)
{
Frame = frame;
}
}
///
/// Specifies the threading context in which code executes or a callback is invoked.
///
public enum ThreadContext
{
///
/// Called from the main thread (or the thread calling World.Step).
/// Safe to access global state.
///
MainThread,
///
/// Called from background worker threads.
/// Code must be thread-safe and lock-free.
///
ParallelWorker,
///
/// Could be called from either. Handle with care.
///
Any
}
///
/// Indicates the thread context in which a callback or event is expected to be invoked.
/// This attribute is primarily informational and used for documentation purposes.
///
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Event)]
public sealed class CallbackThreadAttribute(ThreadContext context) : Attribute
{
///
/// Gets the thread context in which the attributed member is expected to be invoked.
///
public ThreadContext Context { get; } = context;
}
================================================
FILE: src/Jitter2/Collision/CollisionFilter/IBroadPhaseFilter.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
namespace Jitter2.Collision;
///
/// Provides a hook into the broadphase collision detection pipeline.
///
///
/// Implement this interface to intercept shape pairs before narrowphase detection.
/// This can be used to filter out specific pairs, implement custom collision layers,
/// or handle collisions for custom proxy types. See .
///
public interface IBroadPhaseFilter
{
///
/// Called for each pair of proxies whose bounding boxes overlap.
///
/// The first proxy.
/// The second proxy.
/// true to continue with narrowphase detection; false to skip this pair.
[CallbackThread(ThreadContext.Any)]
bool Filter(IDynamicTreeProxy proxyA, IDynamicTreeProxy proxyB);
}
================================================
FILE: src/Jitter2/Collision/CollisionFilter/INarrowPhaseFilter.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
using Jitter2.Collision.Shapes;
using Jitter2.LinearMath;
namespace Jitter2.Collision;
///
/// Provides a hook into the narrowphase collision detection pipeline.
///
///
/// Implement this interface to intercept collisions after contact generation.
/// This can be used to modify contact data, implement custom collision responses,
/// or filter out specific collisions.
///
public interface INarrowPhaseFilter
{
///
/// Called for each detected collision with its contact data.
///
/// The first shape in the collision.
/// The second shape in the collision.
/// Contact point on shape A (modifiable).
/// Contact point on shape B (modifiable).
/// Collision normal from B to A (modifiable).
/// Penetration depth (modifiable).
/// true to keep the collision; false to discard it.
[CallbackThread(ThreadContext.Any)]
bool Filter(RigidBodyShape shapeA, RigidBodyShape shapeB,
ref JVector pointA, ref JVector pointB,
ref JVector normal, ref Real penetration);
}
================================================
FILE: src/Jitter2/Collision/CollisionFilter/TriangleEdgeCollisionFilter.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
// #define DEBUG_EDGEFILTER
using System.Runtime.CompilerServices;
using Jitter2.Collision.Shapes;
using Jitter2.LinearMath;
namespace Jitter2.Collision;
///
/// Filters internal edge collisions for geometry. Adjusts collision
/// normals at shared edges to match neighboring triangles, or discards the collision if the normal
/// cannot be resolved. Requires triangle adjacency information; boundary edges (no neighbor)
/// are left unmodified. Back-face collisions are discarded.
///
public class TriangleEdgeCollisionFilter : INarrowPhaseFilter
{
///
/// Gets or sets the distance threshold for edge collision detection, in world units.
///
///
/// Larger values are more aggressive at filtering edges but may incorrectly affect
/// legitimate collisions near triangle boundaries.
///
/// The default value is 0.01 world units.
public Real EdgeThreshold { get; set; } = (Real)0.01;
private Real cosAngle = (Real)0.999;
///
/// Gets or sets the minimum length of the projected collision normal required to keep the contact.
///
///
/// When the collision normal is projected onto the plane formed by the triangle normal and its
/// neighbor's normal, a very short projection indicates the collision is occurring along the
/// edge crease itself and is discarded. Lower values allow more edge collisions through;
/// higher values are more aggressive at filtering.
///
/// The default value is 0.5.
public Real ProjectionThreshold { get; set; } = (Real)0.5;
///
/// Gets or sets the angle threshold used for two purposes: determining when two triangle normals
/// are considered coplanar (using simpler snapping logic), and discarding back-face collisions
/// whose normal is anti-parallel to the triangle normal within this angle.
///
/// The default value is approximately 2.5 degrees.
public JAngle AngleThreshold
{
get => JAngle.FromRadian(StableMath.Acos(cosAngle));
set => cosAngle = StableMath.Cos(value.Radian);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ProjectPointOnPlane(ref JVector point, in JVector a, in JVector b, in JVector c)
{
var ab = b - a;
var ac = c - a;
var normal = JVector.Cross(ab, ac);
JVector.NormalizeInPlace(ref normal);
var ap = point - a;
var distance = JVector.Dot(ap, normal);
point -= distance * normal;
}
///
public bool Filter(RigidBodyShape shapeA, RigidBodyShape shapeB,
ref JVector pointA, ref JVector pointB, ref JVector normal, ref Real penetration)
{
TriangleShape? ts1 = shapeA as TriangleShape;
TriangleShape? ts2 = shapeB as TriangleShape;
bool c1 = ts1 != null;
bool c2 = ts2 != null;
// both shapes are triangles or both of them are not -> return
if (c1 == c2) return true;
TriangleShape triangleShape;
JVector collP;
if (c1)
{
triangleShape = ts1!;
collP = pointA;
}
else
{
triangleShape = ts2!;
collP = pointB;
}
ref readonly var triangle = ref triangleShape.Mesh.Indices[triangleShape.Index];
JVector tnormal = triangle.Normal;
tnormal = JVector.Transform(tnormal, triangleShape.RigidBody.Data.Orientation);
if (c2) JVector.NegateInPlace(ref tnormal);
// Make triangles penetrable from one side
if (JVector.Dot(normal, tnormal) < -cosAngle) return false;
triangleShape.GetWorldVertices(out JVector a, out JVector b, out JVector c);
// project collP onto triangle plane
ProjectPointOnPlane(ref collP, a, b, c);
var n = b - a;
var pma = collP - a;
var d0 = (pma - JVector.Dot(pma, n) * n * ((Real)1.0 / n.LengthSquared())).LengthSquared();
n = c - a;
pma = collP - a;
var d1 = (pma - JVector.Dot(pma, n) * n * ((Real)1.0 / n.LengthSquared())).LengthSquared();
n = c - b;
pma = collP - b;
var d2 = (pma - JVector.Dot(pma, n) * n * ((Real)1.0 / n.LengthSquared())).LengthSquared();
if (MathR.Min(MathR.Min(d0, d1), d2) > EdgeThreshold * EdgeThreshold) return true;
JVector nnormal;
if (d0 < d1 && d0 < d2)
{
if (triangle.NeighborC == -1) return true;
nnormal = triangleShape.Mesh.Indices[triangle.NeighborC].Normal;
}
else if (d1 < d2)
{
if (triangle.NeighborB == -1) return true;
nnormal = triangleShape.Mesh.Indices[triangle.NeighborB].Normal;
}
else
{
if (triangle.NeighborA == -1) return true;
nnormal = triangleShape.Mesh.Indices[triangle.NeighborA].Normal;
}
nnormal = JVector.Transform(nnormal, triangleShape.RigidBody.Data.Orientation);
ref var b1Data = ref shapeA.RigidBody.Data;
ref var b2Data = ref shapeB.RigidBody.Data;
bool isSpeculative = penetration < (Real)0.0;
if (!isSpeculative)
{
// Check collision again (for non-speculative contacts),
// but with zero epa threshold parameter. This is necessary since
// MPR is not exact for flat shapes, like triangles.
bool result = NarrowPhase.MprEpa(shapeA, shapeB,
b1Data.Orientation, b2Data.Orientation,
b1Data.Position, b2Data.Position,
out pointA, out pointB, out normal, out penetration,
epaThreshold: (Real)0.0);
if (!result)
{
// this should not happen
return false;
}
}
if (c2) JVector.NegateInPlace(ref nnormal);
JVector midPoint = (Real)0.5 * (pointA + pointB);
// now the fun part
//
// we have a collision close to an edge, with
//
// tnormal -> the triangle normal where collision occurred
// nnormal -> the normal of neighboring triangle
// normal -> the collision normal
if (JVector.Dot(tnormal, nnormal) > cosAngle)
{
// tnormal and nnormal are the same
// --------------------------------
Real f5 = JVector.Dot(normal, nnormal);
Real f6 = JVector.Dot(normal, tnormal);
if (f5 > f6)
{
#if DEBUG_EDGEFILTER
Console.WriteLine($"case #1: adjusting; normal {normal} -> {nnormal}");
#endif
if (!isSpeculative)
{
penetration = 0;
pointA = pointB = midPoint;
}
normal = nnormal;
}
else
{
#if DEBUG_EDGEFILTER
Console.WriteLine($"case #1: adjusting; normal {normal} -> {tnormal}");
#endif
if (!isSpeculative)
{
penetration = 0;
pointA = pointB = midPoint;
}
normal = tnormal;
}
return true;
}
// nnormal and tnormal are different
// ----------------------------------
// 1st step, project the normal onto the plane given by tnormal and nnormal
// by removing the component along the cross product axis
JVector cross = nnormal % tnormal;
Real crossLenSq = cross.LengthSquared();
JVector proj = normal - (cross * normal / crossLenSq) * cross;
if (proj.LengthSquared() < ProjectionThreshold * ProjectionThreshold)
{
#if DEBUG_EDGEFILTER
Console.WriteLine($"case #3: discarding");
#endif
// can not project onto the plane, discard
return false;
}
// 2nd step, determine if "proj" is between nnormal and tnormal
//
// / nnormal
// /
// /
// ----- proj
// \
// \
// \ tnormal
Real f1 = proj % nnormal * cross;
Real f2 = proj % tnormal * cross;
bool between = f1 <= (Real)0.0 && f2 >= (Real)0.0;
if (!between)
{
// not in-between, snap normal
Real f3 = JVector.Dot(normal, nnormal);
Real f4 = JVector.Dot(normal, tnormal);
if (f3 > f4)
{
#if DEBUG_EDGEFILTER
Console.WriteLine($"case #2: adjusting; normal {normal} -> {nnormal}");
#endif
if (!isSpeculative)
{
penetration = 0;
pointA = pointB = midPoint;
}
normal = nnormal;
}
else
{
#if DEBUG_EDGEFILTER
Console.WriteLine($"case #2: adjusting; normal {normal} -> {tnormal}");
#endif
if (!isSpeculative)
{
penetration = 0;
pointA = pointB = midPoint;
}
normal = tnormal;
}
}
return true;
}
}
================================================
FILE: src/Jitter2/Collision/CollisionIsland.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
using System.Collections.Generic;
using Jitter2.DataStructures;
using Jitter2.Dynamics;
namespace Jitter2.Collision;
///
/// Represents an island, which is a collection of bodies that are either directly or indirectly in contact with each other.
///
public sealed class Island : IPartitionedSetIndex
{
internal readonly HashSet InternalBodies = [];
internal bool MarkedAsActive;
///
/// Has to be set if an island is active but might contain inactive bodies.
/// This happens, for example, if an inactive and an active island are merged.
///
internal bool NeedsUpdate;
///
/// Gets a collection of all the bodies present in this island.
///
public ReadOnlyHashSet Bodies => new(InternalBodies);
int IPartitionedSetIndex.SetIndex { get; set; } = -1;
///
/// Initializes a new instance of the class.
///
public Island()
{
}
///
/// Clears all the bodies from the lists within this island.
///
internal void ClearLists()
{
InternalBodies.Clear();
}
}
================================================
FILE: src/Jitter2/Collision/DynamicTree/DynamicTree.FindNearest.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Jitter2.Collision.Shapes;
using Jitter2.LinearMath;
namespace Jitter2.Collision;
public partial class DynamicTree
{
///
/// Represents the result of a distance query against the dynamic tree.
///
public struct FindNearestResult
{
/// The closest proxy found.
public IDynamicTreeProxy? Entity;
/// Closest point on the query shape in world space. Undefined when the shapes overlap.
public JVector PointA;
/// Closest point on the found proxy in world space. Undefined when the shapes overlap.
public JVector PointB;
///
/// Unit direction from the query shape toward the found proxy, or
/// when the shapes overlap. Do not use this to test whether a result was found.
///
public JVector Normal;
/// The separation distance between the query shape and the found proxy. Zero when the shapes overlap.
public Real Distance;
}
///
/// Delegate for filtering distance query results after the exact shape distance test.
///
/// The distance result to evaluate.
/// false to filter out this result; true to keep it.
public delegate bool FindNearestFilterPost(FindNearestResult result);
///
/// Delegate for filtering distance query candidates before the exact shape distance test.
///
/// The proxy to evaluate.
/// false to skip this proxy; true to test it.
public delegate bool FindNearestFilterPre(IDynamicTreeProxy proxy);
///
/// Convenience overload of
/// that queries with a .
///
public bool FindNearestPoint(in JVector position,
FindNearestFilterPre? pre, FindNearestFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real distance) =>
FindNearest(SupportPrimitives.CreatePoint(), JQuaternion.Identity, position, pre, post,
out proxy, out pointA, out pointB, out normal, out distance);
///
/// Convenience overload of
/// that queries with a .
///
public bool FindNearestPoint(in JVector position, Real maxDistance,
FindNearestFilterPre? pre, FindNearestFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real distance) =>
FindNearest(SupportPrimitives.CreatePoint(), JQuaternion.Identity, position, maxDistance, pre, post,
out proxy, out pointA, out pointB, out normal, out distance);
///
/// Convenience overload of
/// that queries with a .
///
/// The sphere radius.
public bool FindNearestSphere(Real radius, in JVector position,
FindNearestFilterPre? pre, FindNearestFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real distance) =>
FindNearest(SupportPrimitives.CreateSphere(radius), JQuaternion.Identity, position, pre, post,
out proxy, out pointA, out pointB, out normal, out distance);
///
/// Convenience overload of
/// that queries with a .
///
/// The sphere radius.
public bool FindNearestSphere(Real radius, in JVector position, Real maxDistance,
FindNearestFilterPre? pre, FindNearestFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real distance) =>
FindNearest(SupportPrimitives.CreateSphere(radius), JQuaternion.Identity, position, maxDistance, pre, post,
out proxy, out pointA, out pointB, out normal, out distance);
private struct DistanceQuery(in JBoundingBox box, in JQuaternion orientation, in JVector position)
{
public readonly JBoundingBox Box = box;
public readonly JQuaternion Orientation = orientation;
public readonly JVector Position = position;
public FindNearestFilterPost? FilterPost;
public FindNearestFilterPre? FilterPre;
public Real Distance;
}
///
/// Finds the closest proxy in the tree to the query shape
/// and returns the exact closest points.
///
/// The query shape.
/// The query shape orientation in world space.
/// The query shape position in world space.
/// Optional pre-filter that can skip candidate proxies before the exact distance test.
/// Optional post-filter that can reject exact results and continue the search.
/// The nearest accepted proxy, or null if no proxy is found.
/// Closest point on the query shape in world space. Undefined when the shapes overlap.
/// Closest point on the found proxy in world space. Undefined when the shapes overlap.
/// Unit direction from the query shape toward the found proxy, or when the shapes overlap.
/// Separation distance between the query shape and the found proxy. Zero when they overlap.
///
/// true if an accepted proxy is found. This includes the overlapping case, where
/// is zero. false if no accepted proxy is found, in which case is null.
///
///
/// To skip overlapping proxies and continue searching for separated ones, use a
/// filter that returns false for results with distance == 0.
///
public bool FindNearest(in T support, in JQuaternion orientation, in JVector position,
FindNearestFilterPre? pre, FindNearestFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real distance)
where T : ISupportMappable
{
ShapeHelper.CalculateBoundingBox(support, orientation, position, out JBoundingBox box);
DistanceQuery query = new(box, orientation, position)
{
FilterPre = pre,
FilterPost = post,
Distance = Real.MaxValue
};
bool hit = QueryDistance(support, query, out var result);
proxy = result.Entity;
pointA = result.PointA;
pointB = result.PointB;
normal = result.Normal;
distance = result.Distance;
return hit;
}
///
/// Bounded variant of
/// that limits the search to .
///
/// Maximum separation distance to consider. Proxies farther than this are ignored.
public bool FindNearest(in T support, in JQuaternion orientation, in JVector position, Real maxDistance,
FindNearestFilterPre? pre, FindNearestFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real distance)
where T : ISupportMappable
{
ShapeHelper.CalculateBoundingBox(support, orientation, position, out JBoundingBox box);
DistanceQuery query = new(box, orientation, position)
{
FilterPre = pre,
FilterPost = post,
Distance = maxDistance
};
bool hit = QueryDistance(support, query, out var result);
proxy = result.Entity;
pointA = result.PointA;
pointB = result.PointB;
normal = result.Normal;
distance = result.Distance;
return hit;
}
// Returns the minimum distance between the query AABB and a tree node's expanded AABB.
// Uses the Minkowski sum: expand the target by the query half-extents, then measure
// point-to-AABB distance from the query center. Returns 0 if the AABBs overlap.
private static Real MinDistBox(in JBoundingBox queryBox, in TreeBox targetBox)
{
JVector extents = (queryBox.Max - queryBox.Min) * (Real)0.5;
JVector center = (queryBox.Max + queryBox.Min) * (Real)0.5;
Real dx = MathR.Max(MathR.Max(targetBox.Min.X - extents.X - center.X, center.X - targetBox.Max.X - extents.X), (Real)0.0);
Real dy = MathR.Max(MathR.Max(targetBox.Min.Y - extents.Y - center.Y, center.Y - targetBox.Max.Y - extents.Y), (Real)0.0);
Real dz = MathR.Max(MathR.Max(targetBox.Min.Z - extents.Z - center.Z, center.Z - targetBox.Max.Z - extents.Z), (Real)0.0);
return MathR.Sqrt(dx * dx + dy * dy + dz * dz);
}
private bool QueryDistance(in T support, in DistanceQuery query, out FindNearestResult result)
where T : ISupportMappable
{
result = new FindNearestResult
{
Distance = query.Distance
};
if (root == NullNode)
{
return false;
}
_stack ??= new Stack(256);
_stack.Push(root);
while (_stack.Count > 0)
{
int index = _stack.Pop();
ref Node node = ref nodes[index];
if (node.IsLeaf)
{
if (node.Proxy is not IDistanceTestable distCastable) continue;
if (query.FilterPre != null && !query.FilterPre(node.Proxy!)) continue;
Unsafe.SkipInit(out FindNearestResult res);
bool separated = distCastable.Distance(support,
query.Orientation, query.Position,
out res.PointA, out res.PointB, out res.Normal, out res.Distance);
res.Entity = node.Proxy;
if (!separated)
{
res.Distance = (Real)0.0;
res.Normal = JVector.Zero;
if (query.FilterPost != null && !query.FilterPost(res)) continue;
_stack.Clear();
result = res;
return true;
}
if (res.Distance > result.Distance) continue;
if (query.FilterPost != null && !query.FilterPost(res)) continue;
result = res;
continue;
}
ref Node leftNode = ref nodes[node.Left];
ref Node rightNode = ref nodes[node.Right];
Real leftDist = MinDistBox(query.Box, leftNode.ExpandedBox);
Real rightDist = MinDistBox(query.Box, rightNode.ExpandedBox);
bool leftHit = leftDist <= result.Distance;
bool rightHit = rightDist <= result.Distance;
if (leftHit && rightHit)
{
if (leftDist < rightDist)
{
_stack.Push(node.Right);
_stack.Push(node.Left);
}
else
{
_stack.Push(node.Left);
_stack.Push(node.Right);
}
}
else
{
if (leftHit) _stack.Push(node.Left);
if (rightHit) _stack.Push(node.Right);
}
}
_stack.Clear();
return result.Entity != null;
}
}
================================================
FILE: src/Jitter2/Collision/DynamicTree/DynamicTree.RayCast.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Jitter2.LinearMath;
namespace Jitter2.Collision;
public partial class DynamicTree
{
///
/// Represents the result of a ray cast against the dynamic tree.
///
public struct RayCastResult
{
/// The proxy that was hit.
public IDynamicTreeProxy? Entity;
/// The ray parameter at the hit: hitPoint = origin + Lambda * direction.
public Real Lambda;
///
/// The surface normal at the hit point, or when the ray origin
/// is inside the hit shape. Do not use this to test whether a hit occurred.
///
public JVector Normal;
}
///
/// Delegate for filtering ray cast results after the shape intersection test.
///
/// The ray cast result to evaluate.
/// false to filter out this hit; true to keep it.
public delegate bool RayCastFilterPost(RayCastResult result);
///
/// Delegate for filtering ray cast candidates before the shape intersection test.
///
/// The proxy to evaluate.
/// false to skip this proxy; true to test it.
public delegate bool RayCastFilterPre(IDynamicTreeProxy proxy);
private struct Ray(in JVector origin, in JVector direction)
{
public readonly JVector Origin = origin;
public readonly JVector Direction = direction;
public RayCastFilterPost? FilterPost = null;
public RayCastFilterPre? FilterPre = null;
public Real Lambda = Real.MaxValue;
}
///
/// Ray cast against the world.
///
/// Origin of the ray.
/// Direction of the ray. Does not have to be normalized.
/// Optional pre-filter which allows to skip shapes in the detection.
/// Optional post-filter which allows to skip detections.
/// The shape which was hit.
///
/// The surface normal at the hit point. if the ray does not hit,
/// or if the ray origin is inside the hit shape. Use the return value to determine whether
/// a hit occurred; do not rely on this being non-zero as a hit indicator.
///
/// Distance from the origin to the hit point in units of the ray direction. Zero if the origin is inside the hit shape.
/// True if the ray hits, false otherwise.
public bool RayCast(JVector origin, JVector direction, RayCastFilterPre? pre, RayCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector normal, out Real lambda)
{
Ray ray = new(origin, direction)
{
FilterPre = pre,
FilterPost = post
};
bool hit = QueryRay(ray, out var result);
proxy = result.Entity;
normal = result.Normal;
lambda = result.Lambda;
return hit;
}
///
/// Maximum lambda of the ray's length to consider for intersections.
public bool RayCast(JVector origin, JVector direction, Real maxLambda, RayCastFilterPre? pre, RayCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector normal, out Real lambda)
{
Ray ray = new(origin, direction)
{
FilterPre = pre,
FilterPost = post,
Lambda = maxLambda
};
bool hit = QueryRay(ray, out var result);
proxy = result.Entity;
normal = result.Normal;
lambda = result.Lambda;
return hit;
}
private bool QueryRay(in Ray ray, out RayCastResult result)
{
result = new RayCastResult();
if (root == -1)
{
return false;
}
_stack ??= new Stack(256);
_stack.Push(root);
bool globalHit = false;
result.Lambda = ray.Lambda;
while (_stack.Count > 0)
{
int pop = _stack.Pop();
ref Node node = ref nodes[pop];
if (node.IsLeaf)
{
if (node.Proxy is not IRayCastable irc) continue;
if (ray.FilterPre != null && !ray.FilterPre(node.Proxy)) continue;
Unsafe.SkipInit(out RayCastResult res);
bool hit = irc.RayCast(ray.Origin, ray.Direction, out res.Normal, out res.Lambda);
res.Entity = node.Proxy;
if (hit && res.Lambda < result.Lambda)
{
if (ray.FilterPost != null && !ray.FilterPost(res)) continue;
result = res;
globalHit = true;
}
continue;
}
ref Node lNode = ref nodes[node.Left];
ref Node rNode = ref nodes[node.Right];
bool lRes = lNode.ExpandedBox.RayIntersect(ray.Origin, ray.Direction, out Real lEnter);
bool rRes = rNode.ExpandedBox.RayIntersect(ray.Origin, ray.Direction, out Real rEnter);
if (lEnter > result.Lambda) lRes = false;
if (rEnter > result.Lambda) rRes = false;
if (lRes && rRes)
{
if (lEnter < rEnter)
{
_stack.Push(node.Right);
_stack.Push(node.Left);
}
else
{
_stack.Push(node.Left);
_stack.Push(node.Right);
}
}
else
{
if (lRes) _stack.Push(node.Left);
if (rRes) _stack.Push(node.Right);
}
}
return globalHit;
}
}
================================================
FILE: src/Jitter2/Collision/DynamicTree/DynamicTree.SweepCast.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Jitter2.LinearMath;
using Jitter2.Collision.Shapes;
namespace Jitter2.Collision;
public partial class DynamicTree
{
///
/// Represents the result of a sweep against the dynamic tree.
///
public struct SweepCastResult
{
/// The proxy that was hit.
public IDynamicTreeProxy? Entity;
/// The collision point on the moving query shape in world space at the sweep origin. Undefined when the shapes already overlap.
public JVector PointA;
/// The collision point on the hit object in world space at the sweep origin. Undefined when the shapes already overlap.
public JVector PointB;
///
/// The collision normal in world space, or if the shapes already overlap.
/// Do not use this to test whether a hit occurred.
///
public JVector Normal;
/// The time of impact expressed in units of the sweep direction vector. Zero if the shapes already overlap.
public Real Lambda;
}
///
/// Delegate for filtering sweep results after the exact shape sweep test.
///
/// The sweep result to evaluate.
/// false to filter out this hit; true to keep it.
public delegate bool SweepCastFilterPost(SweepCastResult result);
///
/// Delegate for filtering sweep candidates before the exact shape sweep test.
///
/// The proxy to evaluate.
/// false to skip this proxy; true to test it.
public delegate bool SweepCastFilterPre(IDynamicTreeProxy proxy);
///
/// Convenience overload of
/// that sweeps a .
///
/// The sphere radius.
public bool SweepCastSphere(Real radius, in JVector position, in JVector direction,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda) =>
SweepCast(SupportPrimitives.CreateSphere(radius), JQuaternion.Identity, position, direction, pre,
post, out proxy, out pointA, out pointB, out normal, out lambda);
///
/// Convenience overload of
/// that sweeps a .
///
/// The sphere radius.
public bool SweepCastSphere(Real radius, in JVector position, in JVector direction, Real maxLambda,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda) =>
SweepCast(SupportPrimitives.CreateSphere(radius), JQuaternion.Identity, position, direction, maxLambda, pre,
post, out proxy, out pointA, out pointB, out normal, out lambda);
///
/// Convenience overload of
/// that sweeps a .
///
/// The half extents of the box.
public bool SweepCastBox(in JVector halfExtents, in JQuaternion orientation, in JVector position, in JVector direction,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda) =>
SweepCast(SupportPrimitives.CreateBox(halfExtents), orientation, position, direction, pre,
post, out proxy, out pointA, out pointB, out normal, out lambda);
///
/// Convenience overload of
/// that sweeps a .
///
/// The half extents of the box.
public bool SweepCastBox(in JVector halfExtents, in JQuaternion orientation, in JVector position, in JVector direction, Real maxLambda,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda) =>
SweepCast(SupportPrimitives.CreateBox(halfExtents), orientation, position, direction, maxLambda, pre,
post, out proxy, out pointA, out pointB, out normal, out lambda);
///
/// Convenience overload of
/// that sweeps a .
///
/// The capsule radius.
/// Half the cylindrical section length of the capsule.
public bool SweepCastCapsule(Real radius, Real halfLength, in JQuaternion orientation, in JVector position, in JVector direction,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda) =>
SweepCast(SupportPrimitives.CreateCapsule(radius, halfLength), orientation, position, direction, pre,
post, out proxy, out pointA, out pointB, out normal, out lambda);
///
/// Convenience overload of
/// that sweeps a .
///
/// The capsule radius.
/// Half the cylindrical section length of the capsule.
public bool SweepCastCapsule(Real radius, Real halfLength, in JQuaternion orientation, in JVector position, in JVector direction, Real maxLambda,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda) =>
SweepCast(SupportPrimitives.CreateCapsule(radius, halfLength), orientation, position, direction, maxLambda, pre,
post, out proxy, out pointA, out pointB, out normal, out lambda);
///
/// Convenience overload of
/// that sweeps a .
///
/// The cylinder radius.
/// Half the cylinder height.
public bool SweepCastCylinder(Real radius, Real halfHeight, in JQuaternion orientation, in JVector position, in JVector direction,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda) =>
SweepCast(SupportPrimitives.CreateCylinder(radius, halfHeight), orientation, position, direction, pre,
post, out proxy, out pointA, out pointB, out normal, out lambda);
///
/// Convenience overload of
/// that sweeps a .
///
/// The cylinder radius.
/// Half the cylinder height.
public bool SweepCastCylinder(Real radius, Real halfHeight, in JQuaternion orientation, in JVector position, in JVector direction, Real maxLambda,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda) =>
SweepCast(SupportPrimitives.CreateCylinder(radius, halfHeight), orientation, position, direction, maxLambda, pre,
post, out proxy, out pointA, out pointB, out normal, out lambda);
private struct SweepQuery(in JBoundingBox box, in JQuaternion orientation, in JVector position, in JVector direction)
{
public readonly JBoundingBox Box = box;
public readonly JQuaternion Orientation = orientation;
public readonly JVector Position = position;
public readonly JVector Direction = direction;
public SweepCastFilterPost? FilterPost;
public SweepCastFilterPre? FilterPre;
public Real Lambda;
}
///
/// Sweeps a support-mapped query shape against all proxies in the tree
/// and returns the closest exact hit.
///
/// The query shape.
/// The query shape orientation in world space.
/// The query shape position in world space at the start of the sweep.
/// The sweep direction vector in world space.
/// Optional pre-filter that can skip candidate proxies before the exact sweep test.
/// Optional post-filter that can reject exact results and continue the search.
/// The closest accepted hit proxy, or null if no hit is found.
/// Collision point on the query shape in world space at the sweep origin. Undefined when the shapes already overlap.
/// Collision point on the hit proxy in world space at the sweep origin. Undefined when the shapes already overlap.
/// Collision normal in world space, or when the shapes already overlap.
/// Time of impact in units of . Zero when the shapes already overlap.
///
/// true if an accepted hit is found. This includes the overlapping case, where
/// is zero. false if no accepted hit is found, in which case is null.
///
public bool SweepCast(in T support, in JQuaternion orientation, in JVector position, in JVector direction,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda)
where T : ISupportMappable
{
ShapeHelper.CalculateBoundingBox(support, orientation, position, out JBoundingBox box);
SweepQuery sweep = new(box, orientation, position, direction)
{
FilterPre = pre,
FilterPost = post,
Lambda = Real.MaxValue
};
bool hit = QuerySweep(support, sweep, out var result);
proxy = result.Entity;
pointA = result.PointA;
pointB = result.PointB;
normal = result.Normal;
lambda = result.Lambda;
return hit;
}
///
/// Bounded variant of
/// that limits the sweep to .
///
/// Maximum sweep parameter to consider along .
public bool SweepCast(in T support, in JQuaternion orientation, in JVector position, in JVector direction, Real maxLambda,
SweepCastFilterPre? pre, SweepCastFilterPost? post,
out IDynamicTreeProxy? proxy, out JVector pointA, out JVector pointB, out JVector normal, out Real lambda)
where T : ISupportMappable
{
ShapeHelper.CalculateBoundingBox(support, orientation, position, out JBoundingBox box);
SweepQuery sweep = new(box, orientation, position, direction)
{
FilterPre = pre,
FilterPost = post,
Lambda = maxLambda
};
bool hit = QuerySweep(support, sweep, out var result);
proxy = result.Entity;
pointA = result.PointA;
pointB = result.PointB;
normal = result.Normal;
lambda = result.Lambda;
return hit;
}
private static bool SweepBox(in JBoundingBox movingBox, in JVector translation, in TreeBox targetBox, out Real enter)
{
JVector extents = (movingBox.Max - movingBox.Min) * (Real)0.5;
JVector center = (movingBox.Max + movingBox.Min) * (Real)0.5;
JBoundingBox expanded = new(targetBox.Min - extents, targetBox.Max + extents);
return expanded.RayIntersect(center, translation, out enter);
}
private bool QuerySweep(in T support, in SweepQuery sweep, out SweepCastResult result)
where T : ISupportMappable
{
result = new SweepCastResult
{
Lambda = sweep.Lambda
};
if (root == NullNode)
{
return false;
}
_stack ??= new Stack(256);
_stack.Push(root);
while (_stack.Count > 0)
{
int index = _stack.Pop();
ref Node node = ref nodes[index];
if (node.IsLeaf)
{
if (node.Proxy is not ISweepTestable sweepCastable) continue;
if (sweep.FilterPre != null && !sweep.FilterPre(node.Proxy!)) continue;
Unsafe.SkipInit(out SweepCastResult res);
bool hit = sweepCastable.Sweep(support,
sweep.Orientation, sweep.Position, sweep.Direction,
out res.PointA, out res.PointB, out res.Normal, out res.Lambda);
res.Entity = node.Proxy;
if (!hit || res.Lambda > result.Lambda) continue;
if (sweep.FilterPost != null && !sweep.FilterPost(res)) continue;
result = res;
continue;
}
ref Node leftNode = ref nodes[node.Left];
ref Node rightNode = ref nodes[node.Right];
bool leftHit = SweepBox(sweep.Box, sweep.Direction, leftNode.ExpandedBox, out Real leftEnter);
bool rightHit = SweepBox(sweep.Box, sweep.Direction, rightNode.ExpandedBox, out Real rightEnter);
if (leftEnter > result.Lambda) leftHit = false;
if (rightEnter > result.Lambda) rightHit = false;
if (leftHit && rightHit)
{
if (leftEnter < rightEnter)
{
_stack.Push(node.Right);
_stack.Push(node.Left);
}
else
{
_stack.Push(node.Left);
_stack.Push(node.Right);
}
}
else
{
if (leftHit) _stack.Push(node.Left);
if (rightHit) _stack.Push(node.Right);
}
}
_stack.Clear();
return result.Entity != null;
}
}
================================================
FILE: src/Jitter2/Collision/DynamicTree/DynamicTree.cs
================================================
/*
* Jitter2 Physics Library
* (c) Thorben Linneweber and contributors
* SPDX-License-Identifier: MIT
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Jitter2.DataStructures;
using Jitter2.LinearMath;
using Jitter2.Parallelization;
namespace Jitter2.Collision;
///
/// Represents a dynamic AABB tree for broadphase collision detection.
///
///
/// Uses a bounding volume hierarchy with Surface Area Heuristic (SAH) for O(log n)
/// insertion and removal. Supports incremental updates for moving objects.
///
public partial class DynamicTree
{
private struct OverlapEnumerationParam
{
public Action Action;
public Parallel.Batch Batch;
}
private readonly PartitionedSet proxies = [];
private readonly SlimBag movedProxies = [];
///
/// Gets a read-only view of all proxies registered with this tree.
///
public ReadOnlyPartitionedSet Proxies => new(proxies);
private readonly PairHashSet potentialPairs = [];
///
/// Sentinel value indicating a null/invalid node index.
///
public const int NullNode = -1;
///
/// Initial capacity of the internal node array.
///
public const int InitialSize = 1024;
///
/// Fraction of the potential pairs hash set to scan per update for pruning invalid entries.
/// A value of 128 means 1/128th of the hash set is scanned each update.
///
public const int PruningFraction = 128;
///
/// Specifies the factor by which the bounding box in the dynamic tree structure is expanded. The expansion is calculated as
/// * ExpandFactor * (1 + alpha), where alpha is a pseudo-random number in the range [0, 1).
///
public const Real ExpandFactor = (Real)0.1;
///
/// Specifies a small additional expansion of the bounding box which is constant.
///
public const Real ExpandEps = (Real)0.1;
///
/// Represents a node in the AABB tree.
///
public struct Node
{
/// Index of the left child node, or if this is a leaf.
public int Left;
/// Index of the right child node, or if this is a leaf.
public int Right;
/// Index of the parent node, or if this is the root.
public int Parent;
///
/// The expanded bounding box of this node, used for broadphase culling.
/// For leaf nodes, this is the proxy's bounding box expanded by velocity and a margin.
/// For internal nodes, this is the union of its children's boxes.
///
public TreeBox ExpandedBox;
///
/// The proxy associated with this node, or null for internal nodes.
///
public IDynamicTreeProxy? Proxy;
///
/// When set, forces the node to be updated in the next call,
/// even if its bounding box hasn't changed.
///
public bool ForceUpdate;
///
/// Returns true if this is a leaf node (has an associated proxy).
///
public readonly bool IsLeaf => Proxy != null;
}
private Node[] nodes = new Node[InitialSize];
///
/// Read-only view of the internal tree nodes for debugging or visualization.
/// Do not cache the returned span across tree operations.
///
public ReadOnlySpan Nodes => nodes.AsSpan(0, nodePointer + 1);
private readonly Stack freeNodes = [];
private int nodePointer = -1;
private int root = NullNode;
///
/// Gets the index of the root node, or if the tree is empty.
///
public int Root => root;
private Func filter = null!;
///
/// Gets or sets the filter function used to exclude proxy pairs from collision detection.
///
///
/// The filter is called during overlap enumeration. Return false to exclude a pair.
/// In Jitter, this is typically used to exclude shapes belonging to the same rigid body.
///
/// Thrown when the value is null.
public Func Filter
{
get => filter;
set => filter = value ?? throw new ArgumentNullException(nameof(value));
}
private readonly Action enumerateOverlaps;
private readonly Action updateBoundingBoxes;
private readonly Action scanForMovedProxies;
private readonly Action scanForOverlaps;
private readonly Random random = new(1234);
private readonly Func rndFunc;
///
/// Initializes a new instance of the class.
///
///
/// A function that returns false to exclude a proxy pair from collision detection.
/// See .
///
public DynamicTree(Func filter)
{
enumerateOverlaps = EnumerateOverlapsCallback;
updateBoundingBoxes = UpdateBoundingBoxesCallback;
scanForMovedProxies = ScanForMovedProxies;
scanForOverlaps = ScanForOverlapsCallback;
rndFunc = () => random.NextDouble();
Filter = filter;
}
///
/// Profiling buckets for , representing stages of .
///
public enum Timings
{
/// Time spent removing stale pairs from the potential pairs set.
PruneInvalidPairs,
/// Time spent updating proxy bounding boxes.
UpdateBoundingBoxes,
/// Time spent scanning for proxies that moved outside their expanded boxes.
ScanMoved,
/// Time spent reinserting moved proxies into the tree.
UpdateProxies,
/// Time spent scanning for new overlapping pairs.
ScanOverlaps,
/// Sentinel value for array sizing. Not a real timing bucket.
Last
}
private readonly double[] debugTimings = new double[(int)Timings.Last];
///
/// Contains timings for the stages of the last call to .
/// Values are in milliseconds. Index using (int)Timings.XYZ.
///
public ReadOnlySpan DebugTimings => debugTimings;
///
/// Gets the number of updated proxies during the last call to .
///
public int UpdatedProxyCount => movedProxies.Count;
///
/// Gets the total allocated slot count and the number of entries in the internal hash set
/// used to store potential overlaps.
///
public (int TotalSize, int Count) HashSetInfo => (potentialPairs.Slots.Length, potentialPairs.Count);
private void EnumerateOverlapsCallback(OverlapEnumerationParam parameter)
{
var batch = parameter.Batch;
for (int e = batch.Start; e < batch.End; e++)
{
var node = potentialPairs.Slots[e];
if (node.ID == 0) continue;
var proxyA = nodes[node.ID1].Proxy;
var proxyB = nodes[node.ID2].Proxy;
if(proxyA == null || proxyB == null) continue;
if(!Filter(proxyA, proxyB)) continue;
if (!JBoundingBox.Disjoint(proxyA.WorldBoundingBox, proxyB.WorldBoundingBox))
{
parameter.Action(proxyA, proxyB);
}
}
}
///
/// Enumerates all potential collision pairs and invokes the specified action for each.
///
/// The action to invoke for each overlapping pair.
/// If true, uses multithreading for enumeration.
public void EnumerateOverlaps(Action action, bool multiThread = false)
{
OverlapEnumerationParam overlapEnumerationParam;
overlapEnumerationParam.Action = action;
int slotsLength = potentialPairs.Slots.Length;
if (multiThread)
{
var tpi = ThreadPool.Instance;
// Typically, this is the first multithreaded phase of a simulation step.
// Threads may still be asleep, so we use multiple tasks per thread
// to improve load distribution as they wake up.
const int taskMultiplier = 6;
int taskCount = tpi.ThreadCount * taskMultiplier;
for (int i = 0; i < taskCount; i++)
{
Parallel.GetBounds(slotsLength, taskCount, i, out int start, out int end);
overlapEnumerationParam.Batch = new Parallel.Batch(start, end);
ThreadPool.Instance.AddTask(enumerateOverlaps, overlapEnumerationParam);
}
tpi.Execute();
}
else
{
overlapEnumerationParam.Batch = new Parallel.Batch(0, slotsLength);
EnumerateOverlapsCallback(overlapEnumerationParam);
}
}
///
/// Updates all active proxies in the tree.
///
/// If true, uses multithreading for the update.
/// The timestep in seconds, used for velocity-based bounding box expansion.
public void Update(bool multiThread, Real dt)
{
long time = Stopwatch.GetTimestamp();
double invFrequency = 1.0d / Stopwatch.Frequency;
void SetTime(Timings type)
{
long ctime = Stopwatch.GetTimestamp();
double delta = (ctime - time) * 1000.0d;
debugTimings[(int)type] = delta * invFrequency;
time = ctime;
}
this.stepDt = dt;
Tracer.ProfileBegin(TraceName.PruneInvalidPairs);
PruneInvalidPairs();
Tracer.ProfileEnd(TraceName.PruneInvalidPairs);
SetTime(Timings.PruneInvalidPairs);
if (multiThread)
{
Tracer.ProfileBegin(TraceName.UpdateBoundingBoxes);
proxies.ParallelForBatch(256, updateBoundingBoxes);
Tracer.ProfileEnd(TraceName.UpdateBoundingBoxes);
SetTime(Timings.UpdateBoundingBoxes);
Tracer.ProfileBegin(TraceName.ScanMoved);
movedProxies.Clear();
proxies.ParallelForBatch(24, scanForMovedProxies);
Tracer.ProfileEnd(TraceName.ScanMoved);
SetTime(Timings.ScanMoved);
Tracer.ProfileBegin(TraceName.UpdateProxies);
for (int i = 0; i < movedProxies.Count; i++)
{
InternalAddRemoveProxy(movedProxies[i]);
}
Tracer.ProfileEnd(TraceName.UpdateProxies);
SetTime(Timings.UpdateProxies);
Tracer.ProfileBegin(TraceName.ScanOverlaps);
movedProxies.ParallelForBatch(24, scanForOverlaps);
Tracer.ProfileEnd(TraceName.ScanOverlaps);
SetTime(Timings.ScanOverlaps);
}
else
{
Tracer.ProfileBegin(TraceName.UpdateBoundingBoxes);
var batch = new Parallel.Batch(0, proxies.ActiveCount);
UpdateBoundingBoxesCallback(batch);
Tracer.ProfileEnd(TraceName.UpdateBoundingBoxes);
SetTime(Timings.UpdateBoundingBoxes);
Tracer.ProfileBegin(TraceName.ScanMoved);
movedProxies.Clear();
ScanForMovedProxies(batch);
Tracer.ProfileEnd(TraceName.ScanMoved);
SetTime(Timings.ScanMoved);
Tracer.ProfileBegin(TraceName.UpdateProxies);
for (int i = 0; i < movedProxies.Count; i++)
{
InternalAddRemoveProxy(movedProxies[i]);
}
Tracer.ProfileEnd(TraceName.UpdateProxies);
SetTime(Timings.UpdateProxies);
Tracer.ProfileBegin(TraceName.ScanOverlaps);
ScanForOverlapsCallback(new Parallel.Batch(0, movedProxies.Count));
Tracer.ProfileEnd(TraceName.ScanOverlaps);
SetTime(Timings.ScanOverlaps);
}
// Make sure we do not hold too many dangling references
// in the internal array of the SlimBag data structure which might
// prevent GC. But do only free them one-by-one to prevent overhead.
movedProxies.TrackAndNullOutOne();
}
private Real stepDt;
private void UpdateBoundingBoxesCallback(Parallel.Batch batch)
{
for (int i = batch.Start; i < batch.End; i++)
{
var proxy = proxies[i];
if (proxy is IUpdatableBoundingBox sh) sh.UpdateWorldBoundingBox(stepDt);
}
}
///
/// Forces an immediate update of a single proxy in the tree.
///
/// The proxy type.
/// The proxy to update.
public void Update(T proxy) where T : class, IDynamicTreeProxy
{
if (proxy is IUpdatableBoundingBox sh) sh.UpdateWorldBoundingBox();
OverlapCheckRemove(root, proxy.NodePtr);
InternalRemoveProxy(proxy);
InternalAddProxy(proxy);
OverlapCheckAdd(root, proxy.NodePtr);
}
///
/// Adds a proxy to the tree.
///
/// The proxy type.
/// The proxy to add.
/// If true, the proxy is tracked for movement each update.
/// Thrown if the proxy is already in this tree.
public void AddProxy(T proxy, bool active = true) where T : class, IDynamicTreeProxy
{
if (proxies.Contains(proxy))
{
throw new InvalidOperationException(
$"The proxy '{proxy}' has already been added to this tree instance.");
}
// 2^53 (approx 9e15) is the limit where double-precision values lose integer precision
// (i.e., x + 1.0 == x). Beyond this surface area, the Surface Area Heuristic (SAH)
// cannot detect small changes, causing the tree balancing to degrade.
//
// Note: Since TreeBox calculates surface area using 'double', this assertion
// is valid and necessary for both Single (float) and Double (double) precision builds.
if (proxy.WorldBoundingBox.GetSurfaceArea() > 9.007e15)
{
throw new InvalidOperationException(
$"Added extremely large proxy to dynamic tree. Surface Area exceeds double precision limits (2^53).");
}
InternalAddProxy(proxy);
OverlapCheckAdd(root, proxy.NodePtr);
proxies.Add(proxy, active);
}
///
/// Checks whether the specified proxy is currently active.
///
/// The proxy type.
/// The proxy to check.
/// true if the proxy is active; otherwise, false.
public bool IsActive(T proxy) where T : class, IDynamicTreeProxy
{
return proxies.IsActive(proxy);
}
///
/// Marks a proxy as active, causing it to be tracked for movement during updates.
///
/// The proxy type.
/// The proxy to activate.
public void ActivateProxy(T proxy) where T : class, IDynamicTreeProxy
{
if (proxies.MoveToActive(proxy))
{
nodes[proxy.NodePtr].ForceUpdate = true;
}
}
///
/// Marks a proxy as inactive, assuming it will not move outside its expanded bounding box.
///
/// The proxy type.
/// The proxy to deactivate.
public void DeactivateProxy(T proxy) where T : class, IDynamicTreeProxy
{
proxies.MoveToInactive(proxy);
}
///
/// Removes a proxy from the tree.
///
/// The proxy to remove.
/// Thrown if the proxy is not in this tree.
public void RemoveProxy(IDynamicTreeProxy proxy)
{
if (!proxies.Contains(proxy))
{
throw new InvalidOperationException(
$"The proxy '{proxy}' is not registered with this tree instance.");
}
OverlapCheckRemove(root, proxy.NodePtr);
InternalRemoveProxy(proxy);
proxy.NodePtr = NullNode;
proxies.Remove(proxy);
}
///
/// Calculates the SAH cost of the tree (sum of all node surface areas).
///
/// The total cost. Lower values indicate a more balanced tree.
public double CalculateCost()
{
return Cost(ref nodes[root]);
}
///
/// Enumerates all axis-aligned bounding boxes in the tree.
///
/// The action to perform on each bounding box and node height in the tree.
public void EnumerateTreeBoxes(Action action)
{
if (root == -1) return;
EnumerateTreeBoxes(ref nodes[root], action);
}
private void EnumerateTreeBoxes(ref Node node, Action action, int depth = 1)
{
action(node.ExpandedBox, depth);
if (node.IsLeaf) return;
EnumerateTreeBoxes(ref nodes[node.Left], action, depth + 1);
EnumerateTreeBoxes(ref nodes[node.Right], action, depth + 1);
}
private uint stepper;
///
/// Removes entries from the internal bookkeeping which are both marked as inactive or
/// whose expanded bounding box do not overlap any longer.
/// Only searches a small subset of all elements per call to reduce overhead.
///
private void PruneInvalidPairs()
{
stepper += 1;
for (int i = 0; i < potentialPairs.Slots.Length / PruningFraction; i++)
{
int t = (int)((i * PruningFraction + stepper) % potentialPairs.Slots.Length);
var n = potentialPairs.Slots[t];
if (n.ID == 0) continue;
var proxyA = nodes[n.ID1].Proxy;
var proxyB = nodes[n.ID2].Proxy;
if (proxyA != null && proxyB != null &&
!TreeBox.Disjoint(nodes[proxyA.NodePtr].ExpandedBox, nodes[proxyB.NodePtr].ExpandedBox) &&
(IsActive(proxyA) || IsActive(proxyB)))
{
continue;
}
potentialPairs.Remove(t);
i -= 1;
}
}
[ThreadStatic] private static Stack? _stack;
///
/// Queries the tree for proxies intersecting a ray and appends all hits to the specified sink.
///
/// The sink type receiving all intersected proxies.
/// The sink receiving all intersected proxies.
/// The origin of the ray.
/// The direction of the ray.
public void Query(ref TSink hits, in JVector rayOrigin, in JVector rayDirection)
where TSink : ISink
{
if (root == NullNode) return;
_stack ??= new Stack(256);
_stack.Push(root);
while (_stack.Count > 0)
{
int index = _stack.Pop();
ref Node node = ref nodes[index];
if (node.IsLeaf)
{
if (node.Proxy!.WorldBoundingBox.RayIntersect(rayOrigin, rayDirection, out _))
{
hits.Add(node.Proxy);
}
continue;
}
int left = node.Left;
int right = node.Right;
if (nodes[left].ExpandedBox.RayIntersect(rayOrigin, rayDirection, out _))
{
_stack.Push(left);
}
if (nodes[right].ExpandedBox.RayIntersect(rayOrigin, rayDirection, out _))
{
_stack.Push(right);
}
}
_stack.Clear();
}
///
/// Queries the tree for proxies overlapping an axis-aligned bounding box and appends all hits to the specified sink.
///
/// The sink type receiving all overlapping proxies.
/// The sink receiving all overlapping proxies.
/// The bounding box to query.
public void Query(ref TSink hits, in JBoundingBox box)
where TSink : ISink
{
if (root == NullNode) return;
var sbox = new TreeBox(box);
_stack ??= new Stack(256);
_stack.Push(root);
while (_stack.Count > 0)
{
int index = _stack.Pop();
ref Node node = ref nodes[index];
if (node.IsLeaf)
{
if (!JBoundingBox.Disjoint(node.Proxy!.WorldBoundingBox, box))
{
hits.Add(node.Proxy);
}
continue;
}
int left = node.Left;
int right = node.Right;
if (!TreeBox.Disjoint(nodes[left].ExpandedBox, sbox))
{
_stack.Push(left);
}
if (!TreeBox.Disjoint(nodes[right].ExpandedBox, sbox))
{
_stack.Push(right);
}
}
_stack.Clear();
}
///
/// Queries the tree for proxies intersecting a ray.
///
/// The collection type.
/// Collection to store intersected proxies.
/// The origin of the ray.
/// The direction of the ray.
public void Query(T hits, in JVector rayOrigin, in JVector rayDirection)
where T : class, ICollection
{
var sink = new CollectionSink(hits);
Query(ref sink, in rayOrigin, in rayDirection);
}
///
/// Queries the tree for proxies overlapping an axis-aligned bounding box.
///
/// The collection type.
/// Collection to store overlapping proxies.
/// The bounding box to query.
public void Query(T hits, in JBoundingBox box)
where T : class, ICollection